176 lines
5.4 KiB
JavaScript
176 lines
5.4 KiB
JavaScript
/**
|
||
* Common utilities for the SciPaperLoader application
|
||
*/
|
||
|
||
/**
|
||
* Display a flash message to the user as an overlay
|
||
* @param {string} message - The message to display
|
||
* @param {string} type - The type of message (success, error, warning, info)
|
||
* @param {number} duration - Duration in milliseconds (default: 5000)
|
||
*/
|
||
function showFlashMessage(message, type = "success", duration = 5000) {
|
||
const flashMsg = document.createElement("div");
|
||
const normalizedType = type === "error" ? "danger" : type;
|
||
flashMsg.className = `flash-overlay flash-${normalizedType}`;
|
||
|
||
// Get the appropriate icon based on type
|
||
const getIcon = (messageType) => {
|
||
switch (messageType) {
|
||
case "success":
|
||
return '<svg class="flash-icon" role="img" aria-label="Success:"><use xlink:href="#check-circle-fill"/></svg>';
|
||
case "danger":
|
||
return '<svg class="flash-icon" role="img" aria-label="Error:"><use xlink:href="#x-circle-fill"/></svg>';
|
||
case "warning":
|
||
return '<svg class="flash-icon" role="img" aria-label="Warning:"><use xlink:href="#exclamation-triangle-fill"/></svg>';
|
||
case "info":
|
||
return '<svg class="flash-icon" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>';
|
||
default:
|
||
return '<svg class="flash-icon" role="img" aria-label="Info:"><use xlink:href="#info-fill"/></svg>';
|
||
}
|
||
};
|
||
|
||
flashMsg.innerHTML = `
|
||
<div class="flash-content">
|
||
${getIcon(normalizedType)}
|
||
<div class="flash-message">${message}</div>
|
||
<button type="button" class="flash-close" onclick="removeFlashMessage(this.parentElement.parentElement)">×</button>
|
||
</div>
|
||
`;
|
||
|
||
// Add to page first
|
||
document.body.appendChild(flashMsg);
|
||
|
||
// Position all messages in stack
|
||
updateFlashMessagePositions();
|
||
|
||
// Auto dismiss
|
||
setTimeout(() => {
|
||
removeFlashMessage(flashMsg);
|
||
}, duration);
|
||
|
||
return flashMsg;
|
||
}
|
||
|
||
/**
|
||
* Remove a flash message and update positions
|
||
* @param {HTMLElement} flashMsg - The flash message element to remove
|
||
*/
|
||
function removeFlashMessage(flashMsg) {
|
||
if (!flashMsg || !flashMsg.parentNode) return;
|
||
|
||
flashMsg.classList.add("fade-out");
|
||
setTimeout(() => {
|
||
if (flashMsg.parentNode) {
|
||
flashMsg.remove();
|
||
updateFlashMessagePositions();
|
||
}
|
||
}, 300);
|
||
}
|
||
|
||
/**
|
||
* Update positions of all flash messages to create a proper stack
|
||
*/
|
||
function updateFlashMessagePositions() {
|
||
const messages = document.querySelectorAll(".flash-overlay:not(.fade-out)");
|
||
messages.forEach((msg, index) => {
|
||
const topPosition = 20 + index * 90; // 90px spacing between messages
|
||
msg.style.top = `${topPosition}px`;
|
||
msg.style.zIndex = 9999 - index; // Higher z-index for newer messages
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create a status badge HTML element
|
||
* @param {string} status - The status to create a badge for
|
||
* @returns {string} HTML string for the status badge
|
||
*/
|
||
function createStatusBadge(status) {
|
||
switch (status) {
|
||
case "New":
|
||
return '<span class="badge bg-info">New</span>';
|
||
case "Pending":
|
||
return '<span class="badge bg-warning text-dark">Pending</span>';
|
||
case "Done":
|
||
return '<span class="badge bg-success">Done</span>';
|
||
case "Failed":
|
||
return '<span class="badge bg-danger">Failed</span>';
|
||
case "success":
|
||
return '<span class="badge bg-success">Success</span>';
|
||
case "error":
|
||
return '<span class="badge bg-danger">Error</span>';
|
||
case "pending":
|
||
return '<span class="badge bg-warning text-dark">Pending</span>';
|
||
default:
|
||
return `<span class="badge bg-secondary">${status}</span>`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Format a timestamp to a readable time string
|
||
* @param {string} timestamp - ISO timestamp string
|
||
* @returns {string} Formatted time string
|
||
*/
|
||
function formatTimestamp(timestamp) {
|
||
const date = new Date(timestamp);
|
||
return date.toLocaleTimeString("de-DE", {
|
||
year: "2-digit",
|
||
month: "numeric",
|
||
day: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Truncate text to a specified length
|
||
* @param {string} text - The text to truncate
|
||
* @param {number} maxLength - Maximum length before truncation
|
||
* @returns {string} Truncated text with ellipsis if needed
|
||
*/
|
||
function truncateText(text, maxLength) {
|
||
return text.length > maxLength ? text.substring(0, maxLength) + "..." : text;
|
||
}
|
||
|
||
/**
|
||
* Toggle button loading state
|
||
* @param {HTMLElement} button - The button element
|
||
* @param {boolean} loading - Whether to show loading state
|
||
* @param {string} loadingText - Text to show when loading
|
||
*/
|
||
function toggleButtonLoading(button, loading, loadingText = "Loading...") {
|
||
if (loading) {
|
||
button.disabled = true;
|
||
button.dataset.originalText = button.innerHTML;
|
||
button.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${loadingText}`;
|
||
} else {
|
||
button.disabled = false;
|
||
button.innerHTML = button.dataset.originalText || button.innerHTML;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Generic fetch wrapper with error handling
|
||
* @param {string} url - The URL to fetch
|
||
* @param {object} options - Fetch options
|
||
* @returns {Promise} Fetch promise
|
||
*/
|
||
async function apiRequest(url, options = {}) {
|
||
const defaultOptions = {
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
};
|
||
|
||
const mergedOptions = { ...defaultOptions, ...options };
|
||
|
||
try {
|
||
const response = await fetch(url, mergedOptions);
|
||
const data = await response.json();
|
||
return data;
|
||
} catch (error) {
|
||
console.error(`API request failed for ${url}:`, error);
|
||
throw error;
|
||
}
|
||
}
|