Section 11.1.20 : Mini-projet — Application web complète
🎯 Objectif pédagogique
Synthétiser toutes les compétences de la Semaine 1 en créant une application web fonctionnelle de bout en bout : HTML structuré, CSS stylisé, JavaScript interactif, appel API, et déploiement. C'est le moment de preuve que Marc est prêt pour la Semaine 2.
Le projet : "Job Tracker" — Tableau de bord de candidatures
Marc est en reconversion. Il a besoin de suivre ses candidatures. Plutôt que d'utiliser un fichier Excel (son réflexe d'ex-analyste), il va construire son propre outil — en appliquant tout ce qu'il a appris cette semaine.
Architecture du projet
job-tracker/
├── index.html ← Structure (HTML)
├── style.css ← Design (CSS)
├── app.js ← Logique (JavaScript)
├── README.md ← Documentation
├── .gitignore ← Fichiers à ignorer
└── .env ← Clé API (si IA intégrée)
Étape 1 : HTML — Structure de la page
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job Tracker — Marc Dupont</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>🎯 Job Tracker</h1>
<p>Suivi de candidatures — Bootcamp Semaine 1</p>
</header>
<main>
<section id="add-job">
<h2>Ajouter une candidature</h2>
<form id="job-form">
<input type="text" id="company" placeholder="Entreprise" required>
<input type="text" id="position" placeholder="Poste" required>
<select id="status">
<option value="applied">📨 Candidature envoyée</option>
<option value="interview">🎤 Entretien planifié</option>
<option value="offer">🎉 Offre reçue</option>
<option value="rejected">❌ Refusée</option>
</select>
<input type="date" id="date">
<button type="submit">Ajouter</button>
</form>
</section>
<section id="stats">
<div class="stat-card" id="total-count">
<span class="stat-number">0</span>
<span class="stat-label">Total</span>
</div>
<div class="stat-card" id="interview-count">
<span class="stat-number">0</span>
<span class="stat-label">Entretiens</span>
</div>
<div class="stat-card" id="offer-count">
<span class="stat-number">0</span>
<span class="stat-label">Offres</span>
</div>
</section>
<section id="jobs-list">
<h2>Candidatures</h2>
<div id="jobs-container"></div>
</section>
</main>
<footer>
<p>Construit par Marc Dupont — Semaine 1 du Bootcamp Tech & IA</p>
</footer>
<script src="app.js"></script>
</body>
</html>
Étape 2 : CSS — Design professionnel
* \{ margin: 0; padding: 0; box-sizing: border-box; \}
body \{
font-family: 'Inter', -apple-system, sans-serif;
background: #F5F3EF;
color: #333;
line-height: 1.6;
\}
header \{
background: linear-gradient(135deg, #0891B2, #06b6d4);
color: white;
padding: 2rem;
text-align: center;
\}
main \{ max-width: 900px; margin: 2rem auto; padding: 0 1rem; \}
/* Formulaire */
#job-form \{
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
background: white;
padding: 1.5rem;
border-radius: 12px;
border: 1px solid #E2DFD8;
margin-bottom: 2rem;
\}
#job-form input, #job-form select \{
flex: 1 1 200px;
padding: 0.75rem;
border: 1px solid #E2DFD8;
border-radius: 8px;
font-size: 0.95rem;
\}
#job-form button \{
padding: 0.75rem 2rem;
background: #0891B2;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
\}
#job-form button:hover \{ background: #0e7490; \}
/* Stats */
#stats \{
display: flex;
gap: 1rem;
margin-bottom: 2rem;
\}
.stat-card \{
flex: 1;
background: white;
padding: 1.5rem;
border-radius: 12px;
text-align: center;
border: 1px solid #E2DFD8;
\}
.stat-number \{
display: block;
font-size: 2rem;
font-weight: 700;
color: #0891B2;
\}
.stat-label \{ color: #666; font-size: 0.85rem; \}
/* Job cards */
.job-card \{
background: white;
padding: 1.25rem;
border-radius: 8px;
border: 1px solid #E2DFD8;
margin-bottom: 0.75rem;
display: flex;
justify-content: space-between;
align-items: center;
\}
.job-card .company \{ font-weight: 600; \}
.job-card .position \{ color: #666; \}
.badge \{
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
\}
.badge-applied \{ background: #dbeafe; color: #1d4ed8; \}
.badge-interview \{ background: #fef3c7; color: #92400e; \}
.badge-offer \{ background: #d1fae5; color: #065f46; \}
.badge-rejected \{ background: #fee2e2; color: #991b1b; \}
.delete-btn \{
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
opacity: 0.5;
\}
.delete-btn:hover \{ opacity: 1; \}
@media (max-width: 640px) \{
#stats \{ flex-direction: column; \}
#job-form \{ flex-direction: column; \}
\}
footer \{
text-align: center;
padding: 2rem;
color: #999;
margin-top: 3rem;
\}
Étape 3 : JavaScript — Logique et interactivité
// État de l'application
let jobs = JSON.parse(localStorage.getItem("jobs")) || [];
// Éléments DOM
const form = document.querySelector("#job-form");
const container = document.querySelector("#jobs-container");
// Soumission du formulaire
form.addEventListener("submit", (e) => \{
e.preventDefault();
const job = \{
id: Date.now(),
company: document.querySelector("#company").value.trim(),
position: document.querySelector("#position").value.trim(),
status: document.querySelector("#status").value,
date: document.querySelector("#date").value || new Date().toISOString().split("T")[0]
\};
jobs.push(job);
save();
render();
form.reset();
\});
// Sauvegarder dans localStorage
function save() \{
localStorage.setItem("jobs", JSON.stringify(jobs));
\}
// Supprimer une candidature
function deleteJob(id) \{
jobs = jobs.filter(j => j.id !== id);
save();
render();
\}
// Afficher les stats
function updateStats() \{
document.querySelector("#total-count .stat-number").textContent = jobs.length;
document.querySelector("#interview-count .stat-number").textContent =
jobs.filter(j => j.status === "interview").length;
document.querySelector("#offer-count .stat-number").textContent =
jobs.filter(j => j.status === "offer").length;
\}
// Rendu des cartes
function render() \{
const statusLabels = \{
applied: "📨 Envoyée",
interview: "🎤 Entretien",
offer: "🎉 Offre",
rejected: "❌ Refusée"
\};
container.innerHTML = jobs.map(job => \`
<div class="job-card">
<div>
<span class="company">\$\{job.company\}</span>
<span class="position"> — \$\{job.position\}</span>
<small style="color:#999; margin-left:0.5rem">\$\{job.date\}</small>
</div>
<div style="display:flex; align-items:center; gap:0.5rem">
<span class="badge badge-\$\{job.status\}">\$\{statusLabels[job.status]\}</span>
<button class="delete-btn" onclick="deleteJob(\$\{job.id\})">🗑️</button>
</div>
</div>
\`).join("");
updateStats();
\}
// Rendu initial
render();
Étape 4 : Git + Déploiement
# Initialiser et committer
cd job-tracker
git init
echo "node_modules/\n.env" > .gitignore
git add .
git commit -m "feat: Job Tracker — projet Semaine 1"
# Pousser sur GitHub
git remote add origin https://github.com/votre-username/job-tracker.git
git push -u origin main
# Déployer sur Vercel
# → vercel.com → Import projet → Deploy
Fonctionnalités bonus (optionnel)
- →Filtrage par statut (ajouter des boutons radio)
- →Export CSV des candidatures
- →Mode sombre avec toggle
- →IA : "Analysez mes candidatures" via l'API OpenAI/Groq
🏋️ Récapitulatif Semaine 1
| Compétence | Section | Statut |
|---|---|---|
| Architecture ordinateur | 11.1.1 | ✅ |
| Internet & HTTP | 11.1.2 | ✅ |
| Terminal | 11.1.3 | ✅ |
| Environnement dev (VS Code) | 11.1.4 | ✅ |
| Git | 11.1.5 | ✅ |
| GitHub | 11.1.6 | ✅ |
| Figma | 11.1.7 | ✅ |
| HTML | 11.1.8-11.1.9 | ✅ |
| CSS | 11.1.10-11.1.11 | ✅ |
| JavaScript | 11.1.12-11.1.14 | ✅ |
| Python | 11.1.15-11.1.16 | ✅ |
| APIs | 11.1.17-11.1.18 | ✅ |
| Déploiement | 11.1.19 | ✅ |
| Mini-projet | 11.1.20 | ✅ |
Marc referme son laptop avec un sourire. Il a un portfolio en ligne, un Job Tracker fonctionnel, et les bases pour la suite. En une semaine, il est passé de "je ne sais pas ce qu'est un terminal" à "j'ai déployé une application web". La Semaine 2 s'attaque aux données — pandas, SQL, et visualisation. L'analyste financier en lui a hâte.
Quel est l'avantage d'utiliser localStorage dans le Job Tracker ?
Pourquoi utiliser JSON.stringify() et JSON.parse() avec localStorage ?
L'application Job Tracker utilise toutes les technologies SAUF :