style: Ładny modal potwierdzenia zamiast natywnego confirm()

- Niestandardowy modal z ikoną i animacją
- Przycisk Anuluj i Usuń
- Zamykanie przez klik na overlay lub Escape

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-01-30 20:27:49 +01:00
parent 07358199ea
commit 8bcb339bff

View File

@ -799,6 +799,24 @@
<img id="lightboxImage" src="" alt="Enlarged image">
</div>
<!-- Confirm modal -->
<div class="confirm-modal-overlay" id="confirmModal">
<div class="confirm-modal">
<div class="confirm-modal-icon" id="confirmIcon">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
</div>
<h3 class="confirm-modal-title" id="confirmTitle">Potwierdź usunięcie</h3>
<p class="confirm-modal-message" id="confirmMessage">Czy na pewno chcesz usunąć ten element?</p>
<p class="confirm-modal-warning" id="confirmWarning"></p>
<div class="confirm-modal-actions">
<button type="button" class="btn btn-outline" onclick="closeConfirmModal()">Anuluj</button>
<button type="button" class="btn btn-danger" id="confirmButton">Usuń</button>
</div>
</div>
</div>
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
@ -807,6 +825,85 @@
.toast.warning { border-left-color: var(--warning); }
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
/* Confirm modal */
.confirm-modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 2000;
justify-content: center;
align-items: center;
animation: fadeIn 0.2s ease;
}
.confirm-modal-overlay.active {
display: flex;
}
.confirm-modal {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
max-width: 400px;
width: 90%;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.confirm-modal-icon {
width: 56px;
height: 56px;
border-radius: 50%;
background: #fef2f2;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--spacing-md);
}
.confirm-modal-icon svg {
width: 28px;
height: 28px;
color: #dc2626;
}
.confirm-modal-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.confirm-modal-message {
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.confirm-modal-warning {
font-size: var(--font-size-sm);
color: #dc2626;
margin-bottom: var(--spacing-lg);
}
.confirm-modal-actions {
display: flex;
gap: var(--spacing-md);
justify-content: center;
}
.btn-danger {
background: #dc2626;
color: white;
border: none;
}
.btn-danger:hover {
background: #b91c1c;
}
</style>
{% endblock %}
@ -837,57 +934,93 @@
}
});
// Confirm modal functions
let confirmCallback = null;
function showConfirmModal(title, message, warning, onConfirm) {
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
document.getElementById('confirmWarning').textContent = warning || '';
document.getElementById('confirmModal').classList.add('active');
confirmCallback = onConfirm;
}
function closeConfirmModal() {
document.getElementById('confirmModal').classList.remove('active');
confirmCallback = null;
}
document.getElementById('confirmButton').addEventListener('click', function() {
if (confirmCallback) {
confirmCallback();
}
closeConfirmModal();
});
// Close modal on overlay click
document.getElementById('confirmModal').addEventListener('click', function(e) {
if (e.target === this) {
closeConfirmModal();
}
});
// Admin functions
function deleteTopic(topicId, topicTitle) {
if (!confirm(`Czy na pewno chcesz usunąć wątek "${topicTitle}"?\n\nTa operacja usunie również wszystkie odpowiedzi i jest nieodwracalna.`)) {
return;
}
fetch(`/admin/forum/topic/${topicId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
showConfirmModal(
'Usuń wątek',
`Czy na pewno chcesz usunąć wątek "${topicTitle}"?`,
'Ta operacja usunie również wszystkie odpowiedzi i jest nieodwracalna.',
function() {
fetch(`/admin/forum/topic/${topicId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Wątek usunięty', 'success');
setTimeout(() => window.location.href = '/forum', 1000);
} else {
showToast(data.error || 'Błąd usuwania', 'error');
}
})
.catch(err => {
showToast('Błąd połączenia', 'error');
});
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Wątek usunięty', 'success');
setTimeout(() => window.location.href = '/forum', 1000);
} else {
showToast(data.error || 'Błąd usuwania', 'error');
}
})
.catch(err => {
showToast('Błąd połączenia', 'error');
});
);
}
function deleteReply(replyId) {
if (!confirm('Czy na pewno chcesz usunąć tę odpowiedź?')) {
return;
}
fetch(`/admin/forum/reply/${replyId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
showConfirmModal(
'Usuń odpowiedź',
'Czy na pewno chcesz usunąć tę odpowiedź?',
'Ta operacja jest nieodwracalna.',
function() {
fetch(`/admin/forum/reply/${replyId}/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Odpowiedź usunięta', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Błąd usuwania', 'error');
}
})
.catch(err => {
showToast('Błąd połączenia', 'error');
});
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showToast('Odpowiedź usunięta', 'success');
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Błąd usuwania', 'error');
}
})
.catch(err => {
showToast('Błąd połączenia', 'error');
});
);
}
function togglePin(topicId) {