0

ScanReply #2 : Frontend, Maquettes & TailwindCSS


Dans l’épisode précédent, nous avons posé les fondations de notre projet Django ScanReply. Nous avons maintenant un backend prêt à accueillir notre logique.

Mais avant de plonger dans le code Python complexe (IA, scraping, tâches asynchrones), il est crucial de visualiser ce que nous construisons. Aujourd’hui, on s’attaque au Frontend.

L’objectif est double :

  1. Configurer Django pour gérer les templates et fichiers statiques proprement.
  2. Créer des maquettes HTML “vivantes” pour valider notre UX (Expérience Utilisateur) avant de coder la logique métier.

Nous utiliserons TailwindCSS pour le style. C’est un choix moderne qui permet de prototyper extrêmement vite sans quitter son fichier HTML.


1. Configuration des Templates et Fichiers Statiques

Django a besoin de savoir où chercher nos fichiers HTML et nos assets (CSS, JS, images).

Structure des dossiers

À la racine de votre projet (au même niveau que manage.py), créez deux dossiers :

mkdir templates static

Mise à jour de settings.py

Ouvrez config/settings.py et modifiez les sections suivantes :

Templates : Cherchez la liste TEMPLATES et mettez à jour la clé 'DIRS' :

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'], # <--- Ajoutez ceci
        'APP_DIRS': True,
        # ...
    },
]

Fichiers Statiques : Tout en bas du fichier, configurez les chemins statiques :

STATIC_URL = 'static/'

# Dossiers où Django cherchera les fichiers statiques supplémentaires (comme notre CSS compilé)
STATICFILES_DIRS = [
    BASE_DIR / "static",
]

2. Intégration de TailwindCSS

Pour un projet de qualité “production-ready”, la méthode recommandée est d’utiliser le CLI Tailwind via npm. Cela nous permet d’avoir un fichier CSS final ultra-léger qui ne contient que les classes que nous utilisons réellement.

Initialisation

À la racine du projet, initialisez un projet Node.js (assurez-vous d’avoir Node installé) :

npm init -y
npm install -D tailwindcss
npx tailwindcss init

Cela crée un fichier tailwind.config.js. Ouvrez-le et indiquez à Tailwind où regarder pour trouver vos classes (vos templates HTML !) :

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './templates/**/*.html',
    './**/templates/**/*.html', # Pour les templates dans les apps futures
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Le fichier CSS source

Créez un fichier CSS source dans static/src/input.css (créez le dossier src si nécessaire) :

/* static/src/input.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Script de compilation

Ajoutez un script dans votre package.json pour lancer le “mode watch” de Tailwind. Cela recompilera le CSS automatiquement à chaque modification de vos fichiers HTML.

"scripts": {
  "dev:css": "npx tailwindcss -i ./static/src/input.css -o ./static/css/styles.css --watch"
}

Lancez le compilateur dans un terminal séparé :

npm run dev:css

3. Le Template de Base (base.html)

Tout bon projet Django commence par un base.html dont hériteront toutes nos pages. Créez templates/base.html.

Nous allons inclure :

  • Le lien vers notre CSS compilé.
  • Une police moderne (Inter via Google Fonts).
  • Une structure de navigation simple (Sidebar ou Navbar).
<!-- templates/base.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}ScanReply{% endblock %}</title>
    
    <!-- Google Fonts: Inter -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
    
    <!-- Notre CSS Tailwind compilé -->
    <link href="{% static 'css/styles.css' %}" rel="stylesheet">
    
    <style>
        body { font-family: 'Inter', sans-serif; }
    </style>
</head>
<body class="bg-slate-50 text-slate-900 h-screen flex overflow-hidden">

    <!-- Sidebar de Navigation -->
    <aside class="w-64 bg-slate-900 text-white flex flex-col p-4">
        <div class="mb-8 px-2">
            <h1 class="text-2xl font-bold tracking-tight">Scan<span class="text-blue-500">Reply</span></h1>
        </div>
        
        <nav class="flex-1 space-y-1">
            <a href="{% url 'dashboard' %}" class="flex items-center px-2 py-2 text-sm font-medium rounded-md bg-slate-800 text-white group">
                <svg class="mr-3 h-6 w-6 text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
                </svg>
                Dashboard
            </a>
            
            <a href="{% url 'scan_list' %}" class="flex items-center px-2 py-2 text-sm font-medium rounded-md text-slate-300 hover:bg-slate-800 hover:text-white group">
                <svg class="mr-3 h-6 w-6 text-slate-400 group-hover:text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
                </svg>
                Analyses
            </a>
        </nav>
        
        <div class="mt-auto px-2">
            <div class="text-xs text-slate-500">v0.1.0-alpha</div>
        </div>
    </aside>

    <!-- Contenu Principal -->
    <main class="flex-1 overflow-y-auto p-8">
        {% block content %}
        {% endblock %}
    </main>

</body>
</html>

N’oubliez pas d’ajouter {% load static %} tout en haut de vos templates fils (ou même du base).


4. Les Pages Maquettes

Pour l’instant, nous n’avons pas de modèles. Nous allons utiliser des vues génériques TemplateView pour servir nos pages statiques. Cela permet de valider le design immédiatement.

Dans config/urls.py, ajoutons temporairement :

from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    # ... admin ...
    path('', TemplateView.as_view(template_name='dashboard.html'), name='dashboard'),
    path('scans/', TemplateView.as_view(template_name='scan_list.html'), name='scan_list'),
    path('validate/', TemplateView.as_view(template_name='validate.html'), name='validate'),
]

A. Dashboard (dashboard.html)

Le tableau de bord doit donner une vue d’ensemble immédiate.

<!-- templates/dashboard.html -->
{% extends 'base.html' %}
{% load static %}

{% block content %}
<div class="max-w-7xl mx-auto">
    <h1 class="text-3xl font-bold text-slate-900 mb-8">Vue d'ensemble</h1>

    <!-- Stats Grid -->
    <div class="grid grid-cols-1 gap-5 sm:grid-cols-3">
        <!-- Card 1 -->
        <div class="bg-white overflow-hidden shadow rounded-lg px-4 py-5 sm:p-6">
            <dt class="text-sm font-medium text-slate-500 truncate">Emails non lus</dt>
            <dd class="mt-1 text-3xl font-semibold text-slate-900">12</dd>
        </div>
        
        <!-- Card 2 -->
        <div class="bg-white overflow-hidden shadow rounded-lg px-4 py-5 sm:p-6">
            <dt class="text-sm font-medium text-slate-500 truncate">En attente de validation</dt>
            <dd class="mt-1 text-3xl font-semibold text-indigo-600">4</dd>
        </div>
        
        <!-- Card 3 -->
        <div class="bg-white overflow-hidden shadow rounded-lg px-4 py-5 sm:p-6">
            <dt class="text-sm font-medium text-slate-500 truncate">Réponses envoyées (24h)</dt>
            <dd class="mt-1 text-3xl font-semibold text-green-600">28</dd>
        </div>
    </div>
    
    <!-- Section Activité Récente -->
    <div class="mt-8">
        <h2 class="text-lg leading-6 font-medium text-slate-900 mb-4">Activité récente</h2>
        <div class="bg-white shadow overflow-hidden sm:rounded-md">
            <ul class="divide-y divide-slate-200">
                <!-- Fake Item -->
                <li>
                    <a href="#" class="block hover:bg-slate-50">
                        <div class="px-4 py-4 sm:px-6">
                            <div class="flex items-center justify-between">
                                <p class="text-sm font-medium text-indigo-600 truncate">contact@example.com</p>
                                <div class="ml-2 flex-shrink-0 flex">
                                    <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Envoyé</span>
                                </div>
                            </div>
                            <div class="mt-2 sm:flex sm:justify-between">
                                <div class="sm:flex">
                                    <p class="flex items-center text-sm text-slate-500">
                                        Demande de devis - Ref #4029
                                    </p>
                                </div>
                                <div class="mt-2 flex items-center text-sm text-slate-500 sm:mt-0">
                                    <p>Il y a 2 heures</p>
                                </div>
                            </div>
                        </div>
                    </a>
                </li>
                <!-- Ajoutez-en d'autres pour tester ! -->
            </ul>
        </div>
    </div>
</div>
{% endblock %}

B. Interface de Validation (validate.html)

C’est la page la plus critique. L’IA a généré un brouillon, l’humain doit pouvoir le relire et le modifier, avec le contexte de l’email original à côté.

Utilisons une mise en page en colonnes (Split view).

<!-- templates/validate.html -->
{% extends 'base.html' %}
{% load static %}

{% block content %}
<div class="h-full flex flex-col">
    <!-- Header avec actions -->
    <div class="flex justify-between items-center mb-6">
        <div>
            <h1 class="text-2xl font-bold text-slate-900">Validation de réponse</h1>
            <p class="text-sm text-slate-500">Sujet: Re: Question sur vos tarifs</p>
        </div>
        <div class="flex space-x-3">
            <button class="px-4 py-2 border border-slate-300 shadow-sm text-sm font-medium rounded-md text-slate-700 bg-white hover:bg-slate-50">
                Rejeter / Ignorer
            </button>
            <button class="px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700">
                Valider et Envoyer 🚀
            </button>
        </div>
    </div>

    <!-- Interface Split View -->
    <div class="flex-1 flex gap-6 min-h-0">
        
        <!-- Panneau Gauche : Email Reçu & Info Site -->
        <div class="w-1/2 flex flex-col gap-6 overflow-hidden">
            <!-- Email de l'utilisateur -->
            <div class="bg-white shadow rounded-lg p-6 overflow-y-auto max-h-[50%]">
                <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3">Email reçu</h3>
                <div class="prose prose-slate prose-sm text-slate-800">
                    <p>Bonjour,</p>
                    <p>J'ai vu vos services sur votre site web. Est-ce que vous proposez une API ? Si oui, quels sont les tarifs ?</p>
                    <p>Cordialement,<br>Jean Dupont</p>
                </div>
            </div>

            <!-- Contexte trouvé par le Scraper -->
            <div class="bg-slate-50 border-2 border-dashed border-slate-200 rounded-lg p-4 flex-1 overflow-y-auto">
                <h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">Contexte extrait (Scraping)</h3>
                <ul class="text-sm text-slate-600 space-y-2">
                    <li class="flex items-start">
                        <span class="mr-2">🌍</span> Page visitée : <code>/pricing</code>
                    </li>
                    <li class="flex items-start">
                        <span class="mr-2">💡</span> Info trouvée : "API disponible dans le plan Pro à 29€/mois."
                    </li>
                </ul>
            </div>
        </div>

        <!-- Panneau Droite : Réponse IA -->
        <div class="w-1/2 bg-white shadow rounded-lg flex flex-col">
            <div class="p-4 border-b border-slate-100 flex justify-between items-center bg-indigo-50 rounded-t-lg">
                <span class="text-sm font-medium text-indigo-900">Brouillon suggéré par l'IA</span>
                <span class="text-xs text-indigo-500 bg-indigo-100 px-2 py-1 rounded">Modèle: Mistral-Large</span>
            </div>
            <div class="flex-1 p-0">
                <textarea class="w-full h-full p-6 border-0 focus:ring-0 text-slate-800 resize-none font-sans text-base leading-relaxed" spellcheck="false">Bonjour Jean,

Merci pour votre intérêt !

Oui, nous proposons une API complète. Comme mentionné sur notre page tarifs, l'accès API est inclus à partir de notre plan Pro, qui est actuellement à 29€/mois.

Souhaitez-vous une documentation technique ou une démo ?

Cordialement,
L'équipe Support
                </textarea>
            </div>
        </div>

    </div>
</div>
{% endblock %}

Conclusion

Nous avons maintenant une interface magnifique et fonctionnelle (visuellement) ! 😎

Grâce à TailwindCSS, nous avons pu itérer rapidement sur le design. Séparer l’étape de design de l’étape de code “backend” est une excellente pratique : cela nous permet de valider que nous avons toutes les informations nécessaires à afficher avant de passer du temps à les récupérer en base de données.

Dans le prochain épisode, nous créerons nos Modèles Django et commencerons à injecter de la vraie donnée dans ces pages.

À suivre ! 👋

Commentaires