mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
feat: implement toast notification system for user feedback
This commit is contained in:
@@ -437,6 +437,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Notification Container -->
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
|
||||||
<script src="src/js/config.js"></script>
|
<script src="src/js/config.js"></script>
|
||||||
<script src="src/js/snippet-manager.js"></script>
|
<script src="src/js/snippet-manager.js"></script>
|
||||||
<script src="src/js/dataset-manager.js"></script>
|
<script src="src/js/dataset-manager.js"></script>
|
||||||
|
|||||||
@@ -133,6 +133,83 @@ const AppSettings = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Toast Notification System
|
||||||
|
const Toast = {
|
||||||
|
// Auto-dismiss duration in milliseconds
|
||||||
|
DURATION: 4000,
|
||||||
|
|
||||||
|
// Toast counter for unique IDs
|
||||||
|
toastCounter: 0,
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
show(message, type = 'info') {
|
||||||
|
const container = document.getElementById('toast-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Create toast element
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
const toastId = `toast-${++this.toastCounter}`;
|
||||||
|
toast.id = toastId;
|
||||||
|
toast.className = `toast toast-${type}`;
|
||||||
|
|
||||||
|
// Toast icon based on type
|
||||||
|
const icons = {
|
||||||
|
error: '❌',
|
||||||
|
success: '✓',
|
||||||
|
warning: '⚠️',
|
||||||
|
info: 'ℹ️'
|
||||||
|
};
|
||||||
|
|
||||||
|
toast.innerHTML = `
|
||||||
|
<span class="toast-icon">${icons[type] || icons.info}</span>
|
||||||
|
<span class="toast-message">${message}</span>
|
||||||
|
<button class="toast-close" onclick="Toast.dismiss('${toastId}')">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add to container
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
// Trigger animation
|
||||||
|
setTimeout(() => toast.classList.add('toast-show'), 10);
|
||||||
|
|
||||||
|
// Auto-dismiss
|
||||||
|
setTimeout(() => this.dismiss(toastId), this.DURATION);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Dismiss specific toast
|
||||||
|
dismiss(toastId) {
|
||||||
|
const toast = document.getElementById(toastId);
|
||||||
|
if (!toast) return;
|
||||||
|
|
||||||
|
toast.classList.remove('toast-show');
|
||||||
|
toast.classList.add('toast-hide');
|
||||||
|
|
||||||
|
// Remove from DOM after animation
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) {
|
||||||
|
toast.parentNode.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Convenience methods
|
||||||
|
error(message) {
|
||||||
|
this.show(message, 'error');
|
||||||
|
},
|
||||||
|
|
||||||
|
success(message) {
|
||||||
|
this.show(message, 'success');
|
||||||
|
},
|
||||||
|
|
||||||
|
warning(message) {
|
||||||
|
this.show(message, 'warning');
|
||||||
|
},
|
||||||
|
|
||||||
|
info(message) {
|
||||||
|
this.show(message, 'info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Shared utility: Format bytes for display
|
// Shared utility: Format bytes for display
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === null || bytes === undefined) return 'N/A';
|
if (bytes === null || bytes === undefined) return 'N/A';
|
||||||
|
|||||||
@@ -1164,6 +1164,9 @@ async function deleteCurrentDataset() {
|
|||||||
document.getElementById('dataset-details').style.display = 'none';
|
document.getElementById('dataset-details').style.display = 'none';
|
||||||
window.currentDatasetId = null;
|
window.currentDatasetId = null;
|
||||||
await renderDatasetList();
|
await renderDatasetList();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Toast.success('Dataset deleted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1174,7 +1177,7 @@ async function copyDatasetReference() {
|
|||||||
|
|
||||||
const reference = `"data": {"name": "${dataset.name}"}`;
|
const reference = `"data": {"name": "${dataset.name}"}`;
|
||||||
await navigator.clipboard.writeText(reference);
|
await navigator.clipboard.writeText(reference);
|
||||||
alert('Dataset reference copied to clipboard!');
|
Toast.success('Dataset reference copied to clipboard!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh metadata for URL dataset
|
// Refresh metadata for URL dataset
|
||||||
@@ -1206,7 +1209,7 @@ async function refreshDatasetMetadata() {
|
|||||||
refreshBtn.disabled = false;
|
refreshBtn.disabled = false;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Failed to refresh metadata: ${error.message}`);
|
Toast.error(`Failed to refresh metadata: ${error.message}`);
|
||||||
refreshBtn.textContent = '🔄';
|
refreshBtn.textContent = '🔄';
|
||||||
refreshBtn.disabled = false;
|
refreshBtn.disabled = false;
|
||||||
}
|
}
|
||||||
@@ -1289,8 +1292,11 @@ async function exportCurrentDataset() {
|
|||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Toast.success(`Dataset "${dataset.name}" exported successfully`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Failed to export dataset: ${error.message}`);
|
Toast.error(`Failed to export dataset: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,7 +1332,7 @@ async function importDatasetFromFile(fileInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!format) {
|
if (!format) {
|
||||||
alert('Could not detect data format from file. Please ensure the file contains valid JSON, CSV, or TSV data.');
|
Toast.error('Could not detect data format from file. Please ensure the file contains valid JSON, CSV, or TSV data.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1356,14 +1362,14 @@ async function importDatasetFromFile(fileInput) {
|
|||||||
let data;
|
let data;
|
||||||
if (format === 'json' || format === 'topojson') {
|
if (format === 'json' || format === 'topojson') {
|
||||||
if (!detection.parsed) {
|
if (!detection.parsed) {
|
||||||
alert('Invalid JSON data in file.');
|
Toast.error('Invalid JSON data in file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data = detection.parsed;
|
data = detection.parsed;
|
||||||
} else if (format === 'csv' || format === 'tsv') {
|
} else if (format === 'csv' || format === 'tsv') {
|
||||||
const lines = text.trim().split('\n');
|
const lines = text.trim().split('\n');
|
||||||
if (lines.length < 2) {
|
if (lines.length < 2) {
|
||||||
alert(`${format.toUpperCase()} file must have at least a header row and one data row.`);
|
Toast.error(`${format.toUpperCase()} file must have at least a header row and one data row.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data = text.trim();
|
data = text.trim();
|
||||||
@@ -1383,13 +1389,13 @@ async function importDatasetFromFile(fileInput) {
|
|||||||
|
|
||||||
// Show success message with rename notification if applicable
|
// Show success message with rename notification if applicable
|
||||||
if (wasRenamed) {
|
if (wasRenamed) {
|
||||||
alert(`Dataset name "${baseName}" was already taken, so your dataset was automatically renamed to "${datasetName}".`);
|
Toast.warning(`Dataset name "${baseName}" was already taken, so your dataset was automatically renamed to "${datasetName}".`);
|
||||||
} else {
|
} else {
|
||||||
alert(`Dataset "${datasetName}" imported successfully!`);
|
Toast.success(`Dataset "${datasetName}" imported successfully!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Failed to import dataset: ${error.message}`);
|
Toast.error(`Failed to import dataset: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
// Reset file input
|
// Reset file input
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ const SnippetStorage = {
|
|||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save snippets to localStorage:', error);
|
console.error('Failed to save snippets to localStorage:', error);
|
||||||
alert('Failed to save: Storage quota may be exceeded. Consider deleting old snippets.');
|
Toast.error('Failed to save: Storage quota may be exceeded. Consider deleting old snippets.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -684,11 +684,11 @@ async function openDatasetByName(datasetName) {
|
|||||||
selectDataset(dataset.id);
|
selectDataset(dataset.id);
|
||||||
}, 100);
|
}, 100);
|
||||||
} else {
|
} else {
|
||||||
alert(`Dataset "${datasetName}" not found. It may have been deleted.`);
|
Toast.error(`Dataset "${datasetName}" not found. It may have been deleted.`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error opening dataset:', error);
|
console.error('Error opening dataset:', error);
|
||||||
alert(`Could not open dataset "${datasetName}".`);
|
Toast.error(`Could not open dataset "${datasetName}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -856,6 +856,9 @@ function duplicateSnippet(snippetId) {
|
|||||||
renderSnippetList();
|
renderSnippetList();
|
||||||
selectSnippet(newSnippet.id);
|
selectSnippet(newSnippet.id);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Toast.success('Snippet duplicated successfully');
|
||||||
|
|
||||||
return newSnippet;
|
return newSnippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,14 +894,14 @@ function showExtractModal() {
|
|||||||
|
|
||||||
// Check if spec has inline data
|
// Check if spec has inline data
|
||||||
if (!hasInlineData(spec)) {
|
if (!hasInlineData(spec)) {
|
||||||
alert('No inline data found in this snippet.');
|
Toast.info('No inline data found in this snippet.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the inline data
|
// Extract the inline data
|
||||||
const inlineData = extractInlineDataFromSpec(spec);
|
const inlineData = extractInlineDataFromSpec(spec);
|
||||||
if (!inlineData || inlineData.length === 0) {
|
if (!inlineData || inlineData.length === 0) {
|
||||||
alert('No inline data could be extracted.');
|
Toast.warning('No inline data could be extracted.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,7 +991,7 @@ async function extractToDataset() {
|
|||||||
hideExtractModal();
|
hideExtractModal();
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
alert(`Dataset "${datasetName}" created successfully!`);
|
Toast.success(`Dataset "${datasetName}" created successfully!`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorEl.textContent = `Failed to create dataset: ${error.message}`;
|
errorEl.textContent = `Failed to create dataset: ${error.message}`;
|
||||||
}
|
}
|
||||||
@@ -1025,6 +1028,10 @@ function deleteSnippet(snippetId) {
|
|||||||
|
|
||||||
// Refresh the list
|
// Refresh the list
|
||||||
renderSnippetList();
|
renderSnippetList();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Toast.success('Snippet deleted');
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,6 +1118,9 @@ function publishDraft() {
|
|||||||
restoreSnippetSelection();
|
restoreSnippetSelection();
|
||||||
|
|
||||||
updateViewModeUI(snippet);
|
updateViewModeUI(snippet);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Toast.success('Snippet published successfully!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Revert draft to published spec
|
// Revert draft to published spec
|
||||||
@@ -1133,6 +1143,9 @@ function revertDraft() {
|
|||||||
restoreSnippetSelection();
|
restoreSnippetSelection();
|
||||||
|
|
||||||
updateViewModeUI(snippet);
|
updateViewModeUI(snippet);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Toast.success('Draft reverted to published version');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1177,7 +1190,7 @@ function exportSnippets() {
|
|||||||
const snippets = SnippetStorage.loadSnippets();
|
const snippets = SnippetStorage.loadSnippets();
|
||||||
|
|
||||||
if (snippets.length === 0) {
|
if (snippets.length === 0) {
|
||||||
alert('No snippets to export');
|
Toast.info('No snippets to export');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1196,6 +1209,9 @@ function exportSnippets() {
|
|||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Toast.success(`Exported ${snippets.length} snippet${snippets.length !== 1 ? 's' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize external snippet format to Astrolabe format
|
// Normalize external snippet format to Astrolabe format
|
||||||
@@ -1254,7 +1270,7 @@ function importSnippets(fileInput) {
|
|||||||
const snippetsToImport = Array.isArray(importedData) ? importedData : [importedData];
|
const snippetsToImport = Array.isArray(importedData) ? importedData : [importedData];
|
||||||
|
|
||||||
if (snippetsToImport.length === 0) {
|
if (snippetsToImport.length === 0) {
|
||||||
alert('No snippets found in file');
|
Toast.info('No snippets found in file');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1278,13 +1294,13 @@ function importSnippets(fileInput) {
|
|||||||
|
|
||||||
// Save all snippets
|
// Save all snippets
|
||||||
if (SnippetStorage.saveSnippets(existingSnippets)) {
|
if (SnippetStorage.saveSnippets(existingSnippets)) {
|
||||||
alert(`Successfully imported ${importedCount} snippet${importedCount !== 1 ? 's' : ''}`);
|
Toast.success(`Successfully imported ${importedCount} snippet${importedCount !== 1 ? 's' : ''}`);
|
||||||
renderSnippetList();
|
renderSnippetList();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Import error:', error);
|
console.error('Import error:', error);
|
||||||
alert('Failed to import snippets. Please check that the file is valid JSON.');
|
Toast.error('Failed to import snippets. Please check that the file is valid JSON.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear file input
|
// Clear file input
|
||||||
|
|||||||
@@ -322,3 +322,21 @@ body { font-family: var(--font-main); height: 100vh; overflow: hidden; backgroun
|
|||||||
.help-shortcuts-table td { padding: 12px 8px; font-size: 12px; }
|
.help-shortcuts-table td { padding: 12px 8px; font-size: 12px; }
|
||||||
.shortcut-key { font-family: var(--font-mono); font-weight: bold; background: var(--bg-light); border: 1px solid var(--win-gray-dark); padding: 6px 10px; border-radius: 2px; white-space: nowrap; width: 180px; }
|
.shortcut-key { font-family: var(--font-mono); font-weight: bold; background: var(--bg-light); border: 1px solid var(--win-gray-dark); padding: 6px 10px; border-radius: 2px; white-space: nowrap; width: 180px; }
|
||||||
.shortcut-desc { color: var(--win-gray-darker); }
|
.shortcut-desc { color: var(--win-gray-darker); }
|
||||||
|
|
||||||
|
/* Toast Notifications */
|
||||||
|
#toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 2000; display: flex; flex-direction: column-reverse; gap: 10px; max-width: 400px; }
|
||||||
|
.toast { display: flex; align-items: center; gap: 10px; padding: 12px 16px; background: var(--win-gray); border: 2px outset var(--win-gray); box-shadow: 4px 4px 8px rgba(0,0,0,0.3); font-size: 12px; min-width: 300px; opacity: 0; transform: translateX(400px); transition: all 0.3s ease; }
|
||||||
|
.toast-show { opacity: 1; transform: translateX(0); }
|
||||||
|
.toast-hide { opacity: 0; transform: translateX(400px); }
|
||||||
|
.toast-icon { font-size: 16px; flex-shrink: 0; }
|
||||||
|
.toast-message { flex: 1; line-height: 1.4; color: #000; }
|
||||||
|
.toast-close { background: none; border: none; font-size: 18px; cursor: pointer; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; color: var(--win-gray-darker); flex-shrink: 0; }
|
||||||
|
.toast-close:hover { background: var(--win-gray-dark); color: var(--bg-white); }
|
||||||
|
.toast-error { background: #ffebee; border-color: #c62828; }
|
||||||
|
.toast-error .toast-message { color: #b71c1c; }
|
||||||
|
.toast-success { background: #e8f5e9; border-color: #2e7d32; }
|
||||||
|
.toast-success .toast-message { color: #1b5e20; }
|
||||||
|
.toast-warning { background: #fff3cd; border-color: #ffc107; }
|
||||||
|
.toast-warning .toast-message { color: #856404; }
|
||||||
|
.toast-info { background: #e3f2fd; border-color: #1976d2; }
|
||||||
|
.toast-info .toast-message { color: #0d47a1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user