Files
simplefileupload-server/src/templates/index.html
Matt Hills dc05c90cc4 Replace browser confirm dialog with custom modal
Add custom confirmation modal for file deletion with smooth animations and backdrop blur. Improves UI consistency and provides better user experience than default browser dialogs.
2025-12-27 14:47:37 -05:00

483 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download File</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
color: #2d3748;
position: relative;
}
.container {
background: rgba(255, 255, 255, 0.95);
padding: 2.5rem;
border-radius: 1rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
width: 90%;
max-width: 450px;
backdrop-filter: blur(10px);
transition: transform 0.2s;
}
.container:hover {
transform: translateY(-2px);
}
h1 {
color: #2d3748;
font-size: 1.75rem;
margin-bottom: 1.5rem;
text-align: center;
font-weight: 600;
}
.code-input {
display: flex;
gap: 0.5rem;
justify-content: center;
margin-bottom: 1.5rem;
}
.code-input input {
width: 3.5rem;
height: 3.5rem;
font-size: 1.5rem;
text-align: center;
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
background: white;
outline: none;
transition: all 0.2s;
}
.code-input input:focus {
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.15);
}
.button-group {
display: flex;
gap: 1rem;
align-items: center;
width: 100%;
}
.download-button {
flex: 1;
}
button {
width: 100%;
background-color: #4299e1;
color: white;
padding: 0.75rem;
border: none;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
button:hover {
background-color: #3182ce;
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
button:disabled {
background-color: #a0aec0;
cursor: not-allowed;
transform: none;
}
.delete-button {
background-color: transparent;
color: #718096;
width: auto;
padding: 0.75rem;
transition: all 0.2s;
}
.delete-button:hover {
color: #e53e3e;
background-color: transparent !important;
transform: scale(1.1);
}
.delete-button:disabled {
color: #CBD5E0;
background-color: transparent !important;
}
.delete-button i {
font-size: 1.25rem;
}
.message {
margin-top: 1rem;
padding: 0.75rem;
border-radius: 0.5rem;
text-align: center;
display: none;
}
.message.error {
background-color: #fff5f5;
color: #c53030;
border: 1px solid #feb2b2;
}
.message.success {
background-color: #f0fff4;
color: #2f855a;
border: 1px solid #9ae6b4;
}
.spinner {
display: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid white;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.version {
position: fixed;
bottom: 1rem;
right: 1rem;
font-size: 0.75rem;
color: #718096;
opacity: 0.8;
}
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: white;
border-radius: 1rem;
padding: 2rem;
max-width: 400px;
width: 90%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal h2 {
color: #2d3748;
font-size: 1.5rem;
margin-bottom: 1rem;
font-weight: 600;
}
.modal p {
color: #4a5568;
font-size: 1rem;
margin-bottom: 1.5rem;
line-height: 1.5;
}
.modal-buttons {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
.modal-buttons button {
padding: 0.625rem 1.5rem;
font-size: 0.95rem;
}
.modal-cancel {
background-color: #e2e8f0;
color: #2d3748;
}
.modal-cancel:hover {
background-color: #cbd5e0;
}
.modal-confirm {
background-color: #e53e3e;
}
.modal-confirm:hover {
background-color: #c53030;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('form');
const inputs = document.querySelectorAll('.code-input input');
const downloadButton = document.querySelector('.download-button');
const deleteButton = document.querySelector('.delete-button');
const spinner = document.querySelector('.spinner');
const message = document.querySelector('.message');
const deleteModal = document.getElementById('deleteModal');
const modalCancel = document.getElementById('modalCancel');
const modalConfirm = document.getElementById('modalConfirm');
const showMessage = (text, type) => {
message.textContent = text;
message.className = 'message ' + type;
message.style.display = 'block';
setTimeout(() => {
message.style.display = 'none';
}, 5000);
};
const showDeleteModal = () => {
return new Promise((resolve) => {
deleteModal.classList.add('active');
const handleConfirm = () => {
deleteModal.classList.remove('active');
modalConfirm.removeEventListener('click', handleConfirm);
modalCancel.removeEventListener('click', handleCancel);
deleteModal.removeEventListener('click', handleOverlayClick);
resolve(true);
};
const handleCancel = () => {
deleteModal.classList.remove('active');
modalConfirm.removeEventListener('click', handleConfirm);
modalCancel.removeEventListener('click', handleCancel);
deleteModal.removeEventListener('click', handleOverlayClick);
resolve(false);
};
const handleOverlayClick = (e) => {
if (e.target === deleteModal) {
handleCancel();
}
};
modalConfirm.addEventListener('click', handleConfirm);
modalCancel.addEventListener('click', handleCancel);
deleteModal.addEventListener('click', handleOverlayClick);
});
};
inputs.forEach((input, index) => {
input.addEventListener('input', (e) => {
const value = e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '');
e.target.value = value;
if (value && index < inputs.length - 1) {
inputs[index + 1].focus();
}
const isComplete = Array.from(inputs).every(input => input.value.length === 1);
downloadButton.disabled = !isComplete;
deleteButton.disabled = !isComplete;
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && index > 0) {
inputs[index - 1].focus();
}
});
// Allow pasting the entire code
input.addEventListener('paste', (e) => {
e.preventDefault();
const pastedText = e.clipboardData.getData('text').toUpperCase().replace(/[^A-Z0-9]/g, '');
if (pastedText.length === 6) {
inputs.forEach((input, i) => {
input.value = pastedText[i] || '';
});
downloadButton.disabled = false;
deleteButton.disabled = false;
}
});
});
deleteButton.addEventListener('click', async (e) => {
e.preventDefault();
const code = Array.from(inputs).map(input => input.value).join('');
if (code.length !== 6) {
showMessage('Please enter a valid 6-character code.', 'error');
return;
}
const confirmed = await showDeleteModal();
if (!confirmed) {
return;
}
downloadButton.disabled = true;
deleteButton.disabled = true;
spinner.style.display = 'block';
const originalIcon = deleteButton.innerHTML;
deleteButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
try {
const response = await fetch(`/delete/${code}`, {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
showMessage('File deleted successfully!', 'success');
form.reset();
} else {
showMessage(data.error || 'Error deleting file. Please try again.', 'error');
}
} catch (error) {
showMessage('Error deleting file. Please try again.', 'error');
} finally {
setTimeout(() => {
downloadButton.disabled = false;
deleteButton.disabled = false;
spinner.style.display = 'none';
deleteButton.innerHTML = originalIcon;
}, 2000);
}
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const code = Array.from(inputs).map(input => input.value).join('');
if (code.length !== 6) {
showMessage('Please enter a valid 6-character code.', 'error');
return;
}
downloadButton.disabled = true;
deleteButton.disabled = true;
spinner.style.display = 'block';
downloadButton.textContent = 'Downloading...';
try {
// First check if the code is valid by making a HEAD request
const checkResponse = await fetch(`/download/${code}`, {
method: 'HEAD'
});
if (checkResponse.ok) {
// Code is valid, start download and clear the input
window.location.assign(`/download/${code}`);
showMessage('Download started!', 'success');
// Clear the code inputs after successful download
setTimeout(() => {
form.reset();
downloadButton.disabled = true;
deleteButton.disabled = true;
}, 500);
} else {
// Code is invalid, show error and don't clear
showMessage('Invalid or expired code. Please check and try again.', 'error');
}
} catch (error) {
showMessage('Error starting download. Please try again.', 'error');
} finally {
setTimeout(() => {
downloadButton.disabled = false;
deleteButton.disabled = false;
spinner.style.display = 'none';
downloadButton.textContent = 'Download';
}, 2000);
}
});
});
</script>
</head>
<body>
<div class="container">
<h1>Enter Your 6-Digit Code</h1>
<form action="/download" method="GET">
<div class="code-input">
<input type="text" maxlength="1" required pattern="[A-Za-z0-9]" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" required pattern="[A-Za-z0-9]" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" required pattern="[A-Za-z0-9]" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" required pattern="[A-Za-z0-9]" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" required pattern="[A-Za-z0-9]" inputmode="text" autocomplete="off">
<input type="text" maxlength="1" required pattern="[A-Za-z0-9]" inputmode="text" autocomplete="off">
</div>
<div class="button-group">
<button type="submit" class="download-button" disabled>
Download
<div class="spinner"></div>
</button>
<button type="button" class="delete-button" disabled title="Delete file">
<i class="fas fa-trash-alt"></i>
</button>
</div>
<div class="message"></div>
</form>
</div>
<div class="version">v{{ version }}</div>
<div class="modal-overlay" id="deleteModal">
<div class="modal">
<h2>Delete File</h2>
<p>Are you sure you want to delete this file? This action cannot be undone.</p>
<div class="modal-buttons">
<button type="button" class="modal-cancel" id="modalCancel">Cancel</button>
<button type="button" class="modal-confirm" id="modalConfirm">Delete</button>
</div>
</div>
</div>
</body>
</html>