|
1 | | -<div th:if="${error}" class="error-box" id="errorBox" th:text="${error}"> |
2 | | - <svg class="error-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
3 | | - <circle cx="12" cy="12" r="10"></circle> |
4 | | - <line x1="12" y1="8" x2="12" y2="12"></line> |
5 | | - <line x1="12" y1="16" x2="12" y2="16"></line> |
6 | | - </svg> |
7 | | - <span class="error-message" th:text="${error}">An error has occurred</span> |
8 | | -</div> |
| 1 | +<!-- Notification Container --> |
| 2 | +<div id="notificationContainer" class="fixed top-5 left-1/2 transform -translate-x-1/2 flex flex-col gap-3 z-[9999]"></div> |
9 | 3 |
|
10 | | -<style> |
11 | | - .error-box { |
12 | | - position: fixed; |
13 | | - top: -120px; |
14 | | - left: 50%; |
15 | | - transform: translateX(-50%); |
16 | | - background: linear-gradient(135deg, #ff6a6a, #ff3b3b); |
17 | | - color: #fff; |
18 | | - padding: 1rem 2.5rem 1rem 3.5rem; |
19 | | - border-radius: 12px; |
20 | | - box-shadow: 0 8px 20px rgba(255, 59, 59, 0.6); |
21 | | - font-weight: 700; |
22 | | - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
23 | | - z-index: 10000; |
24 | | - opacity: 0; |
25 | | - pointer-events: none; |
26 | | - transition: all 0.5s cubic-bezier(0.25, 0.8, 0.25, 1); |
27 | | - display: flex; |
28 | | - align-items: center; |
29 | | - gap: 0.8rem; |
30 | | - min-width: 300px; |
31 | | - max-width: 90vw; |
32 | | - } |
| 4 | +<!-- Tailwind + Thymeleaf Error Example --> |
| 5 | +<div th:if="${error}" class="hidden" data-message-type="error" th:data-message-text="${error}"></div> |
| 6 | +<div th:if="${success}" class="hidden" data-message-type="success" th:data-message-text="${success}"></div> |
33 | 7 |
|
34 | | - .error-box.show { |
35 | | - top: 25px; |
36 | | - opacity: 1; |
37 | | - pointer-events: auto; |
38 | | - animation: slideBounce 0.6s ease forwards; |
39 | | - } |
| 8 | +<script> |
| 9 | + function createNotification(message, type = 'error', duration = 5000) { |
| 10 | + const container = document.getElementById('notificationContainer'); |
40 | 11 |
|
41 | | - .error-icon { |
42 | | - width: 24px; |
43 | | - height: 24px; |
44 | | - stroke: #fff; |
45 | | - flex-shrink: 0; |
46 | | - animation: pulse 1.5s infinite; |
47 | | - } |
| 12 | + // Farben und Icons |
| 13 | + const config = { |
| 14 | + error: { |
| 15 | + bg: 'bg-gradient-to-r from-red-600 to-red-500', |
| 16 | + icon: `<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 17 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M12 2a10 10 0 11-0 20 10 10 0 010-20z" /> |
| 18 | + </svg>` |
| 19 | + }, |
| 20 | + success: { |
| 21 | + bg: 'bg-gradient-to-r from-green-500 to-green-400', |
| 22 | + icon: `<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 23 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> |
| 24 | + </svg>` |
| 25 | + } |
| 26 | + }; |
48 | 27 |
|
49 | | - .error-message { |
50 | | - font-size: 1.1rem; |
51 | | - line-height: 1.3; |
52 | | - user-select: none; |
53 | | - } |
| 28 | + const notif = document.createElement('div'); |
| 29 | + notif.className = `${config[type].bg} text-white rounded-xl shadow-lg p-4 flex items-center gap-3 pointer-events-auto transform transition duration-500 scale-95 opacity-0`; |
| 30 | + notif.innerHTML = ` |
| 31 | + ${config[type].icon} |
| 32 | + <span class="font-semibold text-sm md:text-base">${message}</span> |
| 33 | + <button class="ml-auto text-white hover:text-gray-200">×</button> |
| 34 | + `; |
54 | 35 |
|
55 | | - @keyframes slideBounce { |
56 | | - 0% { top: -120px; } |
57 | | - 70% { top: 30px; } |
58 | | - 100% { top: 25px; } |
59 | | - } |
| 36 | + // Close Button |
| 37 | + notif.querySelector('button').addEventListener('click', () => { |
| 38 | + notif.classList.remove('scale-100', 'opacity-100'); |
| 39 | + notif.classList.add('scale-95', 'opacity-0'); |
| 40 | + setTimeout(() => notif.remove(), 400); |
| 41 | + }); |
| 42 | + |
| 43 | + container.appendChild(notif); |
60 | 44 |
|
61 | | - @keyframes pulse { |
62 | | - 0%, 100% { opacity: 1; } |
63 | | - 50% { opacity: 0.6; } |
| 45 | + // Animate in |
| 46 | + setTimeout(() => { |
| 47 | + notif.classList.remove('scale-95', 'opacity-0'); |
| 48 | + notif.classList.add('scale-100', 'opacity-100'); |
| 49 | + }, 50); |
| 50 | + |
| 51 | + // Auto-remove after duration |
| 52 | + setTimeout(() => { |
| 53 | + notif.classList.remove('scale-100', 'opacity-100'); |
| 54 | + notif.classList.add('scale-95', 'opacity-0'); |
| 55 | + setTimeout(() => notif.remove(), 400); |
| 56 | + }, duration); |
64 | 57 | } |
65 | | -</style> |
66 | 58 |
|
67 | | -<script> |
68 | | - window.addEventListener('DOMContentLoaded', () => { |
69 | | - const box = document.getElementById('errorBox'); |
70 | | - if (box) { |
71 | | - setTimeout(() => box.classList.add('show'), 100); |
72 | | - setTimeout(() => box.classList.remove('show'), 5100); |
73 | | - } |
| 59 | + // --- Auto-detect Thymeleaf messages --- |
| 60 | + document.querySelectorAll('[data-message-text]').forEach(el => { |
| 61 | + const type = el.getAttribute('data-message-type'); |
| 62 | + const msg = el.getAttribute('data-message-text'); |
| 63 | + if(msg) createNotification(msg, type); |
74 | 64 | }); |
75 | 65 | </script> |
0 commit comments