update dashboard
This commit is contained in:
@ -7,136 +7,161 @@
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</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">
|
||||
<header class="mb-10 flex justify-between items-center">
|
||||
<h1 class="text-4xl font-extrabold tracking-tight text-white">
|
||||
🧊 Iceberg <span class="text-blue-500">Tracker</span>
|
||||
</h1>
|
||||
<div class="text-sm text-gray-400 bg-gray-800 px-3 py-1 rounded-full border border-gray-700">
|
||||
Statut : <span class="text-green-400">Connecté</span>
|
||||
<header class="mb-10 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<div>
|
||||
<h1 class="text-4xl font-black tracking-tighter text-white italic">
|
||||
ICEBERG <span class="text-blue-500 not-italic">TRACKER</span>
|
||||
</h1>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<section class="bg-gray-800 p-6 rounded-2xl border border-gray-700 shadow-xl mb-10">
|
||||
<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-4">
|
||||
<input type="text" name="name" required placeholder="Nom du produit (ex: Station Soudure)"
|
||||
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 (https://...)"
|
||||
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-bold py-3 px-8 rounded-lg transition duration-200 transform hover:scale-105">
|
||||
Démarrer le tracking
|
||||
<section class="bg-gray-900 p-1 rounded-3xl border border-gray-800 shadow-2xl mb-12">
|
||||
<form th:action="@{/add}" method="POST" class="flex flex-col md:flex-row gap-2 p-2">
|
||||
<input type="text" name="name" required placeholder="Nom de l'objet..."
|
||||
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">
|
||||
<input type="url" name="link" required placeholder="Lien Amazon..."
|
||||
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">
|
||||
<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">
|
||||
Tracker
|
||||
</button>
|
||||
</form>
|
||||
</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}"
|
||||
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="flex items-start gap-4 mb-6">
|
||||
<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'}"
|
||||
class="max-w-full max-h-full object-contain" alt="Image produit">
|
||||
<div class="p-6 flex items-center gap-4 border-b border-gray-800/50">
|
||||
<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">
|
||||
<img th:src="${product.imageUrl ?: 'https://via.placeholder.com/150'}"
|
||||
class="max-w-full max-h-full object-contain" alt="Product">
|
||||
</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 class="flex-1 overflow-hidden">
|
||||
<h3 class="text-lg font-bold text-white truncate" th:text="${product.name}">Nom</h3>
|
||||
<a th:href="${product.link}" target="_blank" class="text-blue-400 text-xs hover:underline truncate block mb-2 italic">
|
||||
Voir sur le site ↗
|
||||
</a>
|
||||
<a th:href="@{'/delete/' + ${product.id}}"
|
||||
onclick="return confirm('Arrêter de suivre ce produit ?')"
|
||||
class="text-red-400 text-xs font-semibold hover:text-red-300 transition">
|
||||
🗑️ Supprimer le tracking
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 pt-4 flex-1">
|
||||
<div class="flex justify-between mb-6 gap-2">
|
||||
<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">Actuel</span>
|
||||
<span th:id="'price-now-' + ${product.id}" class="text-lg font-black text-blue-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">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 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script th:inline="javascript">
|
||||
(function() {
|
||||
const productId = [[${product.id}]];
|
||||
const ctx = document.getElementById('chart-' + productId);
|
||||
const pid = [[${product.id}]];
|
||||
let fullData = [];
|
||||
let chartInstance = null;
|
||||
|
||||
if (ctx) {
|
||||
fetch('/api/prices/' + productId)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (!data || data.length === 0) {
|
||||
// Optionnel : afficher un message si pas de données
|
||||
return;
|
||||
// Fonction globale pour ce produit (attachée à window pour le onclick)
|
||||
window['updateFilter' + pid] = function(days) {
|
||||
const now = new Date();
|
||||
const filtered = days === 0 ? fullData : fullData.filter(d => {
|
||||
const date = new Date(d.dateCheck);
|
||||
return (now - date) / (1000 * 60 * 60 * 24) <= days;
|
||||
});
|
||||
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 => {
|
||||
const date = new Date(d.dateCheck);
|
||||
return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' });
|
||||
});
|
||||
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));
|
||||
}
|
||||
});
|
||||
// Gradient fill
|
||||
chartInstance.data.datasets[0].backgroundColor.addColorStop(0, 'rgba(59, 130, 246, 0.3)');
|
||||
chartInstance.data.datasets[0].backgroundColor.addColorStop(1, 'rgba(59, 130, 246, 0)');
|
||||
chartInstance.update();
|
||||
}
|
||||
|
||||
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>
|
||||
</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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user