feat: implement toast notification system for user feedback

This commit is contained in:
2025-10-16 17:21:51 +03:00
parent c85d0604a9
commit 17d480a2df
5 changed files with 139 additions and 19 deletions

View File

@@ -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>

View File

@@ -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';

View File

@@ -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 = '';

View File

@@ -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

View File

@@ -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; }