update dashboard

This commit is contained in:
Σlie *
2026-02-27 23:08:45 +01:00
parent e691ba4ed9
commit fa64a868e8

View File

@ -7,136 +7,161 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head> </head>
<body class="bg-gray-900 text-gray-100 min-h-screen p-6 md:p-12"> <body class="bg-gray-950 text-gray-100 min-h-screen p-4 md:p-12 font-sans">
<div class="max-w-6xl mx-auto"> <div class="max-w-6xl mx-auto">
<header class="mb-10 flex justify-between items-center"> <header class="mb-10 flex flex-col md:flex-row justify-between items-center gap-4">
<h1 class="text-4xl font-extrabold tracking-tight text-white"> <div>
🧊 Iceberg <span class="text-blue-500">Tracker</span> <h1 class="text-4xl font-black tracking-tighter text-white italic">
</h1> ICEBERG <span class="text-blue-500 not-italic">TRACKER</span>
<div class="text-sm text-gray-400 bg-gray-800 px-3 py-1 rounded-full border border-gray-700"> </h1>
Statut : <span class="text-green-400">Connecté</span> <p class="text-gray-500 text-sm">Surveillance des prix en temps réel</p>
</div>
<div class="flex items-center gap-3 bg-gray-900 border border-gray-800 p-2 rounded-2xl shadow-inner">
<span class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
</span>
<span class="text-xs font-mono text-gray-400 uppercase tracking-widest">System Active</span>
</div> </div>
</header> </header>
<section class="bg-gray-800 p-6 rounded-2xl border border-gray-700 shadow-xl mb-10"> <section class="bg-gray-900 p-1 rounded-3xl border border-gray-800 shadow-2xl mb-12">
<h2 class="text-xl font-semibold mb-4 text-gray-200">Ajouter un produit à surveiller</h2> <form th:action="@{/add}" method="POST" class="flex flex-col md:flex-row gap-2 p-2">
<form th:action="@{/add}" method="POST" class="flex flex-col md:flex-row gap-4"> <input type="text" name="name" required placeholder="Nom de l'objet..."
<input type="text" name="name" required placeholder="Nom du produit (ex: Station Soudure)" class="flex-1 p-4 rounded-2xl bg-gray-800 border-none text-white placeholder-gray-500 focus:ring-2 focus:ring-blue-600 outline-none transition-all">
class="flex-1 p-3 rounded-lg bg-gray-900 border border-gray-600 focus:border-blue-500 focus:outline-none transition"> <input type="url" name="link" required placeholder="Lien Amazon..."
<input type="url" name="link" required placeholder="Lien Amazon (https://...)" class="flex-1 p-4 rounded-2xl bg-gray-800 border-none text-white placeholder-gray-500 focus:ring-2 focus:ring-blue-600 outline-none transition-all">
class="flex-1 p-3 rounded-lg bg-gray-900 border border-gray-600 focus:border-blue-500 focus:outline-none transition"> <button type="submit" class="bg-blue-600 hover:bg-blue-500 text-white font-black py-4 px-10 rounded-2xl transition duration-300 shadow-lg shadow-blue-900/20 uppercase text-sm tracking-widest">
<button type="submit" class="bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-8 rounded-lg transition duration-200 transform hover:scale-105"> Tracker
Démarrer le tracking
</button> </button>
</form> </form>
</section> </section>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div th:each="product : ${products}" th:if="${product != null}" <div th:each="product : ${products}" th:if="${product != null}"
class="bg-gray-800 rounded-2xl border border-gray-700 overflow-hidden shadow-lg hover:border-gray-500 transition duration-300"> class="bg-gray-900 rounded-3xl border border-gray-800 flex flex-col shadow-xl hover:shadow-blue-900/5 transition-all duration-500 group">
<div class="p-6"> <div class="p-6 flex items-center gap-4 border-b border-gray-800/50">
<div class="flex items-start gap-4 mb-6"> <div class="bg-white p-2 rounded-2xl w-20 h-20 flex items-center justify-center shrink-0 shadow-inner group-hover:scale-105 transition-transform">
<div class="bg-white p-2 rounded-lg w-24 h-24 flex items-center justify-center shrink-0"> <img th:src="${product.imageUrl ?: 'https://via.placeholder.com/150'}"
<img th:src="${product.imageUrl ?: 'https://via.placeholder.com/150'}" class="max-w-full max-h-full object-contain" alt="Product">
class="max-w-full max-h-full object-contain" alt="Image produit"> </div>
<div class="flex-1 min-w-0">
<h3 class="text-lg font-bold text-white truncate leading-tight mb-1" th:text="${product.name}">Nom</h3>
<div class="flex gap-3">
<a th:href="${product.link}" target="_blank" class="text-blue-400 text-xs font-bold uppercase tracking-tighter hover:text-blue-300">Amazon ↗</a>
<a th:href="@{'/delete/' + ${product.id}}" onclick="return confirm('Supprimer ?')" class="text-gray-600 text-xs font-bold uppercase tracking-tighter hover:text-red-500">Effacer</a>
</div> </div>
<div class="flex-1 overflow-hidden"> </div>
<h3 class="text-lg font-bold text-white truncate" th:text="${product.name}">Nom</h3> </div>
<a th:href="${product.link}" target="_blank" class="text-blue-400 text-xs hover:underline truncate block mb-2 italic">
Voir sur le site ↗ <div class="p-6 pt-4 flex-1">
</a> <div class="flex justify-between mb-6 gap-2">
<a th:href="@{'/delete/' + ${product.id}}" <div class="bg-gray-800/50 rounded-xl p-2 px-3 flex-1 text-center border border-gray-800">
onclick="return confirm('Arrêter de suivre ce produit ?')" <span class="block text-[10px] text-gray-500 uppercase font-bold tracking-widest mb-1">Actuel</span>
class="text-red-400 text-xs font-semibold hover:text-red-300 transition"> <span th:id="'price-now-' + ${product.id}" class="text-lg font-black text-blue-400">-- €</span>
🗑️ Supprimer le tracking </div>
</a> <div class="bg-gray-800/50 rounded-xl p-2 px-3 flex-1 text-center border border-gray-800">
<span class="block text-[10px] text-gray-500 uppercase font-bold tracking-widest mb-1">Min</span>
<span th:id="'price-min-' + ${product.id}" class="text-lg font-black text-green-400">-- €</span>
</div>
<div class="bg-gray-800/50 rounded-xl p-2 px-3 flex-1 text-center border border-gray-800">
<span class="block text-[10px] text-gray-500 uppercase font-bold tracking-widest mb-1">Max</span>
<span th:id="'price-max-' + ${product.id}" class="text-lg font-black text-red-400">-- €</span>
</div> </div>
</div> </div>
<div class="relative h-64 w-full bg-gray-900/50 rounded-xl p-2"> <div class="flex gap-1 bg-black/30 p-1 rounded-xl mb-4 w-fit mx-auto border border-gray-800">
<button th:onclick="'updateFilter(' + ${product.id} + ', 7)'" class="px-3 py-1 text-[10px] font-bold rounded-lg hover:bg-gray-800 transition-colors uppercase">7j</button>
<button th:onclick="'updateFilter(' + ${product.id} + ', 30)'" class="px-3 py-1 text-[10px] font-bold rounded-lg hover:bg-gray-800 transition-colors uppercase">30j</button>
<button th:onclick="'updateFilter(' + ${product.id} + ', 365)'" class="px-3 py-1 text-[10px] font-bold rounded-lg hover:bg-gray-800 transition-colors uppercase">1 an</button>
<button th:onclick="'updateFilter(' + ${product.id} + ', 0)'" class="px-3 py-1 text-[10px] font-bold rounded-lg bg-blue-600 text-white shadow-lg shadow-blue-900/40 uppercase">Tout</button>
</div>
<div class="relative h-48 w-full">
<canvas th:id="'chart-' + ${product.id}"></canvas> <canvas th:id="'chart-' + ${product.id}"></canvas>
</div> </div>
</div> </div>
<script th:inline="javascript"> <script th:inline="javascript">
(function() { (function() {
const productId = [[${product.id}]]; const pid = [[${product.id}]];
const ctx = document.getElementById('chart-' + productId); let fullData = [];
let chartInstance = null;
if (ctx) { // Fonction globale pour ce produit (attachée à window pour le onclick)
fetch('/api/prices/' + productId) window['updateFilter' + pid] = function(days) {
.then(res => res.json()) const now = new Date();
.then(data => { const filtered = days === 0 ? fullData : fullData.filter(d => {
if (!data || data.length === 0) { const date = new Date(d.dateCheck);
// Optionnel : afficher un message si pas de données return (now - date) / (1000 * 60 * 60 * 24) <= days;
return; });
renderChart(filtered);
// Style visuel des boutons (optionnel)
const btns = document.querySelectorAll(`button[onclick*="updateFilter(${pid}"]`);
btns.forEach(b => b.classList.remove('bg-blue-600', 'text-white'));
event.target.classList.add('bg-blue-600', 'text-white');
};
function renderChart(data) {
if (chartInstance) chartInstance.destroy();
const ctx = document.getElementById('chart-' + pid).getContext('2d');
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => new Date(d.dateCheck).toLocaleDateString('fr-FR', {day:'2-digit', month:'short'})),
datasets: [{
data: data.map(d => d.price),
borderColor: '#3b82f6',
borderWidth: 4,
pointRadius: 0,
pointHoverRadius: 6,
pointHoverBackgroundColor: '#fff',
fill: true,
backgroundColor: ctx.createLinearGradient(0, 0, 0, 200),
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { display: false, beginAtZero: false },
x: { grid: { display: false }, ticks: { color: '#4b5563', font: {size: 9} } }
} }
}
// Extraction des labels (dates) et des prix });
const labels = data.map(d => { // Gradient fill
const date = new Date(d.dateCheck); chartInstance.data.datasets[0].backgroundColor.addColorStop(0, 'rgba(59, 130, 246, 0.3)');
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }); chartInstance.data.datasets[0].backgroundColor.addColorStop(1, 'rgba(59, 130, 246, 0)');
}); chartInstance.update();
const prices = data.map(d => d.price);
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Prix (€)',
data: prices,
borderColor: '#3b82f6', // Bleu
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderWidth: 3,
fill: true,
tension: 0.4, // Courbe lisse
pointRadius: 4,
pointBackgroundColor: '#3b82f6'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#1f2937',
titleColor: '#9ca3af',
bodyColor: '#ffffff',
borderColor: '#374151',
borderWidth: 1,
displayColors: false
}
},
scales: {
y: {
beginAtZero: false,
grid: { color: 'rgba(255, 255, 255, 0.05)' },
ticks: { color: '#9ca3af' }
},
x: {
grid: { display: false },
ticks: { color: '#9ca3af' }
}
}
}
});
})
.catch(err => console.error("Erreur API:", err));
} }
fetch('/api/prices/' + pid)
.then(res => res.json())
.then(data => {
if (!data.length) return;
fullData = data;
// Maj des stats
const prices = data.map(d => d.price);
document.getElementById('price-now-' + pid).innerText = prices[prices.length-1] + ' €';
document.getElementById('price-min-' + pid).innerText = Math.min(...prices) + ' €';
document.getElementById('price-max-' + pid).innerText = Math.max(...prices) + ' €';
renderChart(data);
});
})(); })();
// Petit fix pour relier le onclick au scope global
window['updateFilter'] = (id, days) => window['updateFilter' + id](days);
</script> </script>
</div> </div>
</div> </div>
<div th:if="${#lists.isEmpty(products)}" class="text-center py-20 bg-gray-800 rounded-3xl border-2 border-dashed border-gray-700">
<p class="text-gray-400 text-xl italic">Aucun produit en cours de surveillance. Ajoutez-en un au-dessus !</p>
</div>
</div> </div>
</body> </body>
</html> </html>