Files
MaterielPartage/index.php
2025-12-26 12:10:08 +01:00

788 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
session_start();
$db = new PDO('sqlite:db.sqlite');
$CATEGORIES = [
"Bricolage",
"Jardinage",
"Cuisine",
"Outils lourds",
"Matériel événementiel",
"Transport",
"Sport",
"Voyage",
"Autre"
];
$action = $_GET['action'] ?? 'home';
function redirect($a) {
header("Location: index.php?action=$a");
exit;
}
function is_logged() {
return isset($_SESSION['owner_id']);
}
/* ============================================================
TRAITEMENT DES FORMULAIRES
============================================================ */
/* ---------- INSCRIPTION ---------- */
if ($action === 'do_register' && $_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$stmt = $db->prepare("
INSERT INTO owners (name, address, phone, email, username, password_hash)
VALUES (?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$_POST['name'],
$_POST['address'],
$_POST['phone'],
$_POST['email'],
$_POST['username'],
password_hash($_POST['password'], PASSWORD_DEFAULT)
]);
$_SESSION['owner_id'] = $db->lastInsertId();
redirect('edit');
} catch (PDOException $e) {
$error = "Nom d'utilisateur déjà pris.";
$action = 'register';
}
}
/* ---------- CONNEXION ---------- */
if ($action === 'do_login' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $db->prepare("SELECT * FROM owners WHERE username=?");
$stmt->execute([$_POST['username']]);
$owner = $stmt->fetch(PDO::FETCH_ASSOC);
if ($owner && password_verify($_POST['password'], $owner['password_hash'])) {
$_SESSION['owner_id'] = $owner['id'];
redirect('edit');
} else {
$error = "Identifiants incorrects.";
$action = 'login';
}
}
/* ---------- DÉCONNEXION ---------- */
if ($action === 'logout') {
session_destroy();
redirect('home');
}
/* ---------- SAUVEGARDE MATERIEL (prix numérique + prix libre) ---------- */
if ($action === 'save_item' && $_SERVER['REQUEST_METHOD'] === 'POST' && is_logged()) {
// Prix numérique obligatoire
$price = floatval($_POST['price']);
if ($price <= 0) {
$error = "Le prix doit être un nombre positif.";
$action = 'edit';
}
// Prix libre ?
$price_free = isset($_POST['price_free']) ? "free" : "";
$price_value = $price_free ? "$price|free" : "$price";
// Upload photo
$photo = null;
if (!empty($_FILES['photo']['name'])) {
$photo = time() . "_" . basename($_FILES['photo']['name']);
move_uploaded_file($_FILES['photo']['tmp_name'], "uploads/" . $photo);
}
// UPDATE
if (!empty($_POST['id'])) {
if ($photo) {
$stmt = $db->prepare("UPDATE items SET name=?, price=?, description=?, photo=?, category=? WHERE id=? AND owner_id=?");
$stmt->execute([
$_POST['name'],
$price_value,
$_POST['description'],
$photo,
$_POST['category'],
$_POST['id'],
$_SESSION['owner_id']
]);
} else {
$stmt = $db->prepare("UPDATE items SET name=?, price=?, description=?, category=? WHERE id=? AND owner_id=?");
$stmt->execute([
$_POST['name'],
$price_value,
$_POST['description'],
$_POST['category'],
$_POST['id'],
$_SESSION['owner_id']
]);
}
// INSERT
} else {
$stmt = $db->prepare("INSERT INTO items (name, price, description, photo, owner_id, category)
VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([
$_POST['name'],
$price_value,
$_POST['description'],
$photo,
$_SESSION['owner_id'],
$_POST['category']
]);
}
redirect('edit');
}
/* ---------- SUPPRESSION MATERIEL ---------- */
if ($action === 'delete_item' && is_logged()) {
$stmt = $db->prepare("DELETE FROM items WHERE id=? AND owner_id=?");
$stmt->execute([$_GET['id'], $_SESSION['owner_id']]);
redirect('edit');
}
/* ---------- GENERATION STATISTIQUES ---------- */
if ($action === 'admin_generate_stats') {
// Sécurité : seul toi (ID=1)
if (!isset($_SESSION['owner_id']) || $_SESSION['owner_id'] != 1) {
echo "<p>Accès refusé.</p>";
exit;
}
// Emplacement du rapport généré
$report = __DIR__ . "/stats.html";
// Commande GoAccess
$cmd = "goaccess /var/log/nginx/materiel.kabano.org-access.log "
. "--log-format=COMBINED "
. "-o " . escapeshellarg($report) . " 2>&1";
// Exécuter la commande
$output = shell_exec($cmd);
// Vérifier si le fichier a été généré
if (!file_exists($report)) {
echo "<h3>Erreur lors de la génération du rapport</h3>";
echo "<pre>$output</pre>";
exit;
}
// Redirection vers le rapport
header("Location: stats.html");
exit;
}
/* ---------- SUPPRESSION PHOTO MATERIEL ---------- */
if ($action === 'delete_photo' && is_logged()) {
// Récupérer le matériel
$stmt = $db->prepare("SELECT photo FROM items WHERE id=? AND owner_id=?");
$stmt->execute([$_GET['id'], $_SESSION['owner_id']]);
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if ($item && !empty($item['photo'])) {
$file = "uploads/" . $item['photo'];
if (file_exists($file)) {
unlink($file);
}
// Mettre la colonne photo à NULL
$stmt = $db->prepare("UPDATE items SET photo=NULL WHERE id=? AND owner_id=?");
$stmt->execute([$_GET['id'], $_SESSION['owner_id']]);
}
redirect('edit_item&id=' . $_GET['id']);
}
/* ---------- MODIFICATION PROFIL ---------- */
if ($action === 'save_profile' && $_SERVER['REQUEST_METHOD'] === 'POST' && is_logged()) {
$params = [
$_POST['name'],
$_POST['address'],
$_POST['phone'],
$_POST['email'],
$_POST['username']
];
$sql = "UPDATE owners SET name=?, address=?, phone=?, email=?, username=?";
if (!empty($_POST['password'])) {
$sql .= ", password_hash=?";
$params[] = password_hash($_POST['password'], PASSWORD_DEFAULT);
}
$sql .= " WHERE id=?";
$params[] = $_SESSION['owner_id'];
$stmt = $db->prepare($sql);
$stmt->execute($params);
$success = "Profil mis à jour.";
$action = 'profile';
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Location de matériel</title>
<link rel="stylesheet" href="pico.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🧰</text></svg>">
<style>
body { max-width: 900px; margin: 40px auto; }
.item { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 1.5rem; }
.tool-row { display: flex; gap: 1.5rem; align-items: flex-start; }
.tool-row img { width: 25%; max-width: 180px; border-radius: 8px; object-fit: cover; }
.tool-info { flex: 1; }
.owner-block { margin-top: .5rem; font-size: .9rem; color: #555; }
</style>
</head>
<body style="padding: 0 10px">
<h1 style="display:flex; align-items:center; justify-content:space-between;">
<span>Matériel partagé à Durban-sur-Arize</span>
<img src="uploads/blason-durban.png" alt="Blason de Durban-sur-Arize"
style="height:48px; width:auto; margin-left:1rem;">
</h1>
<nav>
<a href="index.php">Accueil</a>
<?php if (is_logged()): ?>
<a href="index.php?action=edit">Mon matériel</a>
<a href="index.php?action=profile">Mon profil</a>
<?php if (isset($_SESSION['owner_id']) && $_SESSION['owner_id'] == 1): ?>
<a href="index.php?action=admin">Admin</a>
<?php endif; ?>
<a href="index.php?action=logout">Déconnexion</a>
<?php else: ?>
<a href="index.php?action=login">Connexion</a>
<a href="index.php?action=register">Inscription</a>
<?php endif; ?>
</nav>
<hr>
<?php
/* ============================================================
VUE : ACCUEIL (liste publique)
============================================================ */
if ($action === 'home') {
?>
<div id="price-customizer" style="
margin-bottom: 1.2rem;
padding: .6rem .8rem;
border: 1px solid var(--pico-muted-border-color);
border-radius: .5rem;
font-size: .8rem;
">
<form id="income-form" style="
display: flex;
align-items: center;
gap: .4rem;
flex-wrap: wrap;
margin: 0;
">
<strong>Personnaliser les indications de prix libre</strong>
<span>selon mon revenu mensuel :</span>
<input
type="number"
id="income"
value="1900"
min="0"
step="1"
style="width: 110px; margin: 0; height: 0.8rem; font-size: 0.8rem;">
<span>€/mois</span>
</form>
</div>
<div style="text-align: justify;"><small>L'objectif du site est de mettre en relation des emprunteur·ses et loueur·ses avec des propriétaires de matériel. Des tarifs peuvent être indiqués, ils restent avant tout indicatifs afin déviter les malaises et de poser une base aux discussions. Lessentiel est de privilégier la discussion, la confiance, les arrangements et l'entraide. L'idée n'est pas de faire une grande publicité pour cet outil de partage, mais de le garder à petite échelle, localement, autour de valeurs de solidarité et de proximité. N'hésitez pas à être le plus clair possible dès le début sur les issues d'une casse.</small></div>
<div id="search-tools" style="
margin: 1.2rem 0;
padding: .6rem .8rem;
border: 1px solid var(--pico-muted-border-color);
border-radius: .5rem;
font-size: .9rem;
">
<form style="display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; margin:0;">
<strong style="font-size:.95rem;">Rechercher un matériel :</strong>
<input
type="text"
id="search"
placeholder="Nom ou description…"
style="flex:1; min-width:200px; margin:0;"
>
</form>
</div>
<?php
$items = $db->query("
SELECT items.*, owners.name AS owner_name, owners.phone, owners.address, owners.email
FROM items
JOIN owners ON owners.id = items.owner_id
ORDER BY items.category COLLATE NOCASE ASC,
items.name COLLATE NOCASE ASC
")->fetchAll(PDO::FETCH_ASSOC);
foreach ($items as $item) {
// Décodage du prix
$price_raw = $item['price'] ?? '';
$price_parts = explode('|', $price_raw);
$price_value = floatval($price_parts[0]);
$price_free = isset($price_parts[1]) && $price_parts[1] === 'free';
$search_text = strtolower(
($item['name'] ?? '') . ' ' . ($item['description'] ?? '')
);
echo "<article class='item' data-search='" . htmlspecialchars($search_text, ENT_QUOTES) . "'>";
echo "<div class='tool-row'>";
// Photo
if (!empty($item['photo'])) {
echo "<img src='uploads/{$item['photo']}' alt='Photo'>";
} else {
echo "<div style='
width:25%; max-width: 180px;
height:120px;
display:flex;
align-items:center;
justify-content:center;
font-size:60px;
'>🛠️</div>";
}
echo "<div class='tool-info'>";
echo "<h2 style='margin: 0;'>" . htmlspecialchars($item['name'] ?? '') . "</h2>";
echo "<p style='opacity:.7; font-size:.85rem;'>Catégorie : " . htmlspecialchars($item['category']) . "</p>";
// Prix
echo "<p class='price-block' data-base-price='{$price_value}' data-free='{$price_free}'>";
echo "<strong>Prix :</strong> {$price_value} € / jour";
if ($price_free) echo " <em>(prix libre)</em>";
echo "</p>";
// Description
echo "<p>" . nl2br(htmlspecialchars($item['description'] ?? '')) . "</p>";
// Propriétaire
echo "<div class='owner-block'>";
echo "<strong>Propriétaire :</strong> " . htmlspecialchars($item['owner_name'] ?? '') . "<br>";
// Adresse
$addr = urlencode($item['address'] ?? '');
echo htmlspecialchars($item['address'] ?? '')."<br>";
// Téléphone → tel:
echo "<a href='tel:" . htmlspecialchars($item['phone'] ?? '') . "'>"
. htmlspecialchars($item['phone'] ?? '')
. "</a><br>";
// Email → mailto:
echo "<a href='mailto:" . htmlspecialchars($item['email'] ?? '') . "'>"
. htmlspecialchars($item['email'] ?? '')
. "</a>";
echo "</div>"; // owner-block
echo "</div>"; // tool-info
echo "</div>"; // tool-row
echo "</article>";
}
}
/* ============================================================
VUE : CONNEXION
============================================================ */
if ($action === 'login') {
echo "<h2>Connexion</h2>";
if (!empty($error)) echo "<p style='color:red'>$error</p>";
?>
<form method="post" action="index.php?action=do_login">
<input name="username" placeholder="Nom d'utilisateur" required>
<input name="password" type="password" placeholder="Mot de passe" required>
<button>Connexion</button>
</form>
<?php
}
/* ============================================================
VUE : INSCRIPTION
============================================================ */
if ($action === 'register') {
echo "<h2>Inscription</h2>";
if (!empty($error)) echo "<p style='color:red'>$error</p>";
?>
<form method="post" action="index.php?action=do_register">
<small>Toutes les coordonnées indiquées ici seront affichées publiquement.<br><br></small>
<input name="name" placeholder="Nom complet" required>
<input name="address" placeholder="Commune (éviter une adresse précise)">
<input name="phone" placeholder="Téléphone">
<input name="email" placeholder="Email">
<input name="username" placeholder="Nom d'utilisateur" required>
<input name="password" type="password" placeholder="Mot de passe" required>
<button>Créer mon compte</button>
</form>
<?php
}
/* ============================================================
VUE : INSCRIPTION
============================================================ */
if ($action === 'admin' && isset($_SESSION['owner_id']) && $_SESSION['owner_id'] == 1) {
$users = $db->query("SELECT id, name, email FROM owners ORDER BY id ASC")->fetchAll(PDO::FETCH_ASSOC);
?>
<h2>Administration — Utilisateurs</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>Nom</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<?php foreach ($users as $u): ?>
<tr>
<td><?= htmlspecialchars($u['id']) ?></td>
<td><?= htmlspecialchars($u['name']) ?></td>
<td><?= htmlspecialchars($u['email']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<h2>Statistiques</h2>
<p> <a href="index.php?action=admin_generate_stats">🔄 Générer le rapport GoAccess</a> </p>
<?php
}
/* ============================================================
VUE : PROFIL
============================================================ */
if ($action === 'profile' && is_logged()) {
$stmt = $db->prepare("SELECT * FROM owners WHERE id=?");
$stmt->execute([$_SESSION['owner_id']]);
$owner = $stmt->fetch(PDO::FETCH_ASSOC);
echo "<h2>Mon profil</h2>";
if (!empty($success)) echo "<p style='color:green'>$success</p>";
?>
<form method="post" action="index.php?action=save_profile">
<small>Toutes les coordonnées indiquées ici seront affichées publiquement.<br><br></small>
<input name="name" value="<?= htmlspecialchars($owner['name'] ?? '') ?>" placeholder="Nom complet" required>
<input name="address" value="<?= htmlspecialchars($owner['address'] ?? '') ?>" placeholder="Commune (éviter une adresse précise)">
<input name="phone" value="<?= htmlspecialchars($owner['phone'] ?? '') ?>" placeholder="Téléphone">
<input name="email" value="<?= htmlspecialchars($owner['email'] ?? '') ?>" placeholder="Email">
<input name="username" value="<?= htmlspecialchars($owner['username'] ?? '') ?>" placeholder="Nom d'utilisateur" required>
<input name="password" type="password" placeholder="Nouveau mot de passe (laisser vide)">
<button>Mettre à jour</button>
</form>
<?php
}
/* ============================================================
VUE : ÉDITION DUN MATERIEL
============================================================ */
if ($action === 'edit_item' && is_logged()) {
$stmt = $db->prepare("SELECT * FROM items WHERE id=? AND owner_id=?");
$stmt->execute([$_GET['id'], $_SESSION['owner_id']]);
$item = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$item) {
echo "<p>Matériel introuvable.</p>";
} else {
// Décodage du prix
$price_raw = $item['price'] ?? '';
$price_parts = explode('|', $price_raw);
$price_value = floatval($price_parts[0]);
$price_free = isset($price_parts[1]) && $price_parts[1] === 'free';
echo "<h2>Modifier le matériel</h2>";
?>
<form method="post" enctype="multipart/form-data" action="index.php?action=save_item">
<input type="hidden" name="id" value="<?= $item['id'] ?>">
<label>Nom du matériel</label>
<input name="name" value="<?= htmlspecialchars($item['name']) ?>" required>
<label>Prix par jour (€)</label>
<input name="price" type="number" step="0.01" min="0"
value="<?= $price_value ?>" required>
<p style="font-size:.85rem; opacity:.8;">
Le prix libre conseillé sera ajusté selon le revenu de lutilisateur. Prix saisi pour le revenu médian de 1900€.
</p>
<label>
<input type="checkbox" name="price_free" <?= $price_free ? 'checked' : '' ?>>
Prix libre (le prix devient indicatif)
</label>
<label>Catégorie</label>
<select name="category" required>
<option value="">Choisir…</option>
<?php foreach ($CATEGORIES as $cat): ?>
<option value="<?= htmlspecialchars($cat) ?>"
<?= ($item['category'] === $cat ? 'selected' : '') ?>>
<?= htmlspecialchars($cat) ?>
</option>
<?php endforeach; ?>
</select>
<label>Description</label>
<textarea name="description"><?= htmlspecialchars($item['description']) ?></textarea>
<label>Photo actuelle</label><br>
<?php if ($item['photo']): ?>
<img src="uploads/<?= $item['photo'] ?>" style="max-width:150px;border-radius:6px;">
<br>
<a href="index.php?action=delete_photo&id=<?= $item['id'] ?>"
onclick="return confirm('Supprimer cette photo ?')"
style="font-size:.85rem; color:var(--pico-del-color);">
Supprimer la photo
</a>
<?php else: ?>
<div style='
width:150px;
height:100px;
display:flex;
align-items:center;
justify-content:center;
font-size:50px;
'>🛠️</div>
<?php endif; ?>
<br><br>
<label>Nouvelle photo (optionnel)</label>
<input type="file" name="photo">
<button>Mettre à jour</button>
</form>
<?php
}
}
/* ============================================================
VUE : GESTION DES MATERIELS
============================================================ */
if ($action === 'edit' && is_logged()) {
echo "<h2>Mes matériels</h2>";
?>
<h3 style="cursor:pointer;" id="toggle-form"> Ajouter un matériel</h3>
<div id="tool-form" style="display:none; margin-bottom:2rem;">
<form method="post" enctype="multipart/form-data" action="index.php?action=save_item">
<input type="hidden" name="id">
<label>Nom du matériel</label>
<input name="name" placeholder="Nom" required>
<label>Prix par jour (€)</label>
<input name="price" type="number" step="0.01" min="0" placeholder="Prix" required>
<p style="font-size:.85rem; opacity:.8;">
Le prix libre conseillé sera ajusté selon le revenu de lutilisateur. Prix saisi pour le revenu médian de 1900€.
</p>
<label>
<input type="checkbox" name="price_free">
Prix libre (le prix devient indicatif)
</label>
<label>Catégorie</label>
<select name="category" required>
<option value="">Choisir…</option>
<?php foreach ($CATEGORIES as $cat): ?>
<option value="<?= htmlspecialchars($cat) ?>"><?= htmlspecialchars($cat) ?></option>
<?php endforeach; ?>
</select>
<label>Description</label>
<textarea name="description" placeholder="Description"></textarea>
<label>Photo</label>
<input type="file" name="photo">
<button>Enregistrer</button>
</form>
</div>
<?php
/* Liste des matériels existants */
$stmt = $db->prepare("SELECT * FROM items WHERE owner_id=? ORDER BY category COLLATE NOCASE ASC,
name COLLATE NOCASE ASC
");
$stmt->execute([$_SESSION['owner_id']]);
$items = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "<h3>Mes matériels existants</h3>";
foreach ($items as $item) {
// Décodage du prix
$price_raw = $item['price'] ?? '';
$price_parts = explode('|', $price_raw);
$price_value = floatval($price_parts[0]);
$price_free = isset($price_parts[1]) && $price_parts[1] === 'free';
echo "<article class='item'>";
echo "<div class='tool-row'>";
// Photo
if (!empty($item['photo'])) {
echo "<img src='uploads/{$item['photo']}' alt='Photo'>";
} else {
echo "<div style='
width:25%; max-width: 180px;
height:120px;
display:flex;
align-items:center;
justify-content:center;
font-size:60px;
'>🛠️</div>";
}
echo "<div class='tool-info'>";
echo "<h3 style='margin: 0;'>" . htmlspecialchars($item['name'] ?? '') . "</h3>";
echo "<p style='opacity:.7; font-size:.85rem;'>Catégorie : " . htmlspecialchars($item['category']) . "</p>";
echo "<p><strong>Prix :</strong> {$price_value} € / jour";
if ($price_free) echo " <em>(prix libre)</em>";
echo "</p>";
echo "<p>" . nl2br(htmlspecialchars($item['description'] ?? '')) . "</p>";
echo "<footer>";
echo "<a href='index.php?action=edit_item&id={$item['id']}'>Modifier</a> | ";
echo "<a href='index.php?action=delete_item&id={$item['id']}' onclick='return confirm(\"Supprimer ce matériel ?\")'>Supprimer</a>";
echo "</footer>";
echo "</div>"; // tool-info
echo "</div>"; // tool-row
echo "</article>";
}
}
?>
<script>
// -----------------------------
// Recalcul des prix libres
// -----------------------------
function updateFreePrices() {
const incomeInput = document.getElementById('income');
if (!incomeInput) return; // Pas de champ -> rien à faire
const income = parseFloat(incomeInput.value) || 1900;
const ratio = income / 1900;
document.querySelectorAll('.price-block').forEach(block => {
const base = parseFloat(block.dataset.basePrice);
const isFree = block.dataset.free === "1" || block.dataset.free === "true";
if (!isFree) return;
const newPrice = (base * ratio).toFixed(2);
block.innerHTML = `<strong>Prix :</strong> ${newPrice} € / jour <em>(prix libre)</em>`;
});
}
// Mise à jour en direct si le champ existe
const incomeInput = document.getElementById('income');
if (incomeInput) {
incomeInput.addEventListener('input', updateFreePrices);
updateFreePrices(); // Mise à jour initiale
}
// Empêcher Enter dans le formulaire income
const incomeForm = document.getElementById('income-form');
if (incomeForm) {
incomeForm.addEventListener('keydown', function (e) {
if (e.key === "Enter") e.preventDefault();
});
}
// -----------------------------
// Recherche dynamique
// -----------------------------
function updateSearch() {
const searchInput = document.getElementById('search');
if (!searchInput) return;
const query = searchInput.value.toLowerCase().trim();
const words = query.split(/\s+/).filter(w => w.length > 0);
document.querySelectorAll('article.item').forEach(item => {
const text = item.dataset.search || "";
const match = words.every(w => text.includes(w));
item.style.display = match ? "" : "none";
});
}
const searchInput = document.getElementById('search');
if (searchInput) {
searchInput.addEventListener('input', updateSearch);
searchInput.addEventListener('keydown', function (e) {
if (e.key === "Enter") e.preventDefault();
});
}
// -----------------------------
// Dépliage du formulaire matériel
// -----------------------------
const toggleForm = document.getElementById('toggle-form');
const toolForm = document.getElementById('tool-form');
if (toggleForm && toolForm) {
toggleForm.addEventListener('click', function () {
toolForm.style.display =
(toolForm.style.display === "none" || toolForm.style.display === "")
? "block"
: "none";
});
}
</script>
<footer style="margin-top:2rem; text-align:center; opacity:.7; font-size:.85rem;">
Problème ou question :
<a href="mailto:leo@kabano.org">Léo / leo@kabano.org</a>
</footer>
</body>
</html>