From 17d480a2df7f5dd2fadbc2ae8bd4ea70fba77f23 Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Thu, 16 Oct 2025 17:21:51 +0300 Subject: [PATCH] feat: implement toast notification system for user feedback --- index.html | 3 ++ src/js/config.js | 77 +++++++++++++++++++++++++++++++++++++++ src/js/dataset-manager.js | 24 +++++++----- src/js/snippet-manager.js | 36 +++++++++++++----- src/styles.css | 18 +++++++++ 5 files changed, 139 insertions(+), 19 deletions(-) diff --git a/index.html b/index.html index 3081cfe..d561818 100644 --- a/index.html +++ b/index.html @@ -437,6 +437,9 @@ + +
+ diff --git a/src/js/config.js b/src/js/config.js index 9128214..3304b8d 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -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 = ` + ${icons[type] || icons.info} + ${message} + + `; + + // 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 function formatBytes(bytes) { if (bytes === null || bytes === undefined) return 'N/A'; diff --git a/src/js/dataset-manager.js b/src/js/dataset-manager.js index 34d2c99..f3fd023 100644 --- a/src/js/dataset-manager.js +++ b/src/js/dataset-manager.js @@ -1164,6 +1164,9 @@ async function deleteCurrentDataset() { document.getElementById('dataset-details').style.display = 'none'; window.currentDatasetId = null; await renderDatasetList(); + + // Show success message + Toast.success('Dataset deleted'); } } @@ -1174,7 +1177,7 @@ async function copyDatasetReference() { const reference = `"data": {"name": "${dataset.name}"}`; await navigator.clipboard.writeText(reference); - alert('Dataset reference copied to clipboard!'); + Toast.success('Dataset reference copied to clipboard!'); } // Refresh metadata for URL dataset @@ -1206,7 +1209,7 @@ async function refreshDatasetMetadata() { refreshBtn.disabled = false; }, 1000); } catch (error) { - alert(`Failed to refresh metadata: ${error.message}`); + Toast.error(`Failed to refresh metadata: ${error.message}`); refreshBtn.textContent = '🔄'; refreshBtn.disabled = false; } @@ -1289,8 +1292,11 @@ async function exportCurrentDataset() { document.body.removeChild(link); URL.revokeObjectURL(url); + // Show success message + Toast.success(`Dataset "${dataset.name}" exported successfully`); + } 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) { - 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; } @@ -1356,14 +1362,14 @@ async function importDatasetFromFile(fileInput) { let data; if (format === 'json' || format === 'topojson') { if (!detection.parsed) { - alert('Invalid JSON data in file.'); + Toast.error('Invalid JSON data in file.'); return; } data = detection.parsed; } else if (format === 'csv' || format === 'tsv') { const lines = text.trim().split('\n'); 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; } data = text.trim(); @@ -1383,13 +1389,13 @@ async function importDatasetFromFile(fileInput) { // Show success message with rename notification if applicable 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 { - alert(`Dataset "${datasetName}" imported successfully!`); + Toast.success(`Dataset "${datasetName}" imported successfully!`); } } catch (error) { - alert(`Failed to import dataset: ${error.message}`); + Toast.error(`Failed to import dataset: ${error.message}`); } finally { // Reset file input fileInput.value = ''; diff --git a/src/js/snippet-manager.js b/src/js/snippet-manager.js index 4107e59..1a78cea 100644 --- a/src/js/snippet-manager.js +++ b/src/js/snippet-manager.js @@ -157,7 +157,7 @@ const SnippetStorage = { return true; } catch (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; } }, @@ -684,11 +684,11 @@ async function openDatasetByName(datasetName) { selectDataset(dataset.id); }, 100); } else { - alert(`Dataset "${datasetName}" not found. It may have been deleted.`); + Toast.error(`Dataset "${datasetName}" not found. It may have been deleted.`); } } catch (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(); selectSnippet(newSnippet.id); + // Show success message + Toast.success('Snippet duplicated successfully'); + return newSnippet; } @@ -891,14 +894,14 @@ function showExtractModal() { // Check if spec has inline data if (!hasInlineData(spec)) { - alert('No inline data found in this snippet.'); + Toast.info('No inline data found in this snippet.'); return; } // Extract the inline data const inlineData = extractInlineDataFromSpec(spec); if (!inlineData || inlineData.length === 0) { - alert('No inline data could be extracted.'); + Toast.warning('No inline data could be extracted.'); return; } @@ -988,7 +991,7 @@ async function extractToDataset() { hideExtractModal(); // Show success message - alert(`Dataset "${datasetName}" created successfully!`); + Toast.success(`Dataset "${datasetName}" created successfully!`); } catch (error) { errorEl.textContent = `Failed to create dataset: ${error.message}`; } @@ -1025,6 +1028,10 @@ function deleteSnippet(snippetId) { // Refresh the list renderSnippetList(); + + // Show success message + Toast.success('Snippet deleted'); + return true; } @@ -1111,6 +1118,9 @@ function publishDraft() { restoreSnippetSelection(); updateViewModeUI(snippet); + + // Show success message + Toast.success('Snippet published successfully!'); } // Revert draft to published spec @@ -1133,6 +1143,9 @@ function revertDraft() { restoreSnippetSelection(); updateViewModeUI(snippet); + + // Show success message + Toast.success('Draft reverted to published version'); } } @@ -1177,7 +1190,7 @@ function exportSnippets() { const snippets = SnippetStorage.loadSnippets(); if (snippets.length === 0) { - alert('No snippets to export'); + Toast.info('No snippets to export'); return; } @@ -1196,6 +1209,9 @@ function exportSnippets() { link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); + + // Show success message + Toast.success(`Exported ${snippets.length} snippet${snippets.length !== 1 ? 's' : ''}`); } // Normalize external snippet format to Astrolabe format @@ -1254,7 +1270,7 @@ function importSnippets(fileInput) { const snippetsToImport = Array.isArray(importedData) ? importedData : [importedData]; if (snippetsToImport.length === 0) { - alert('No snippets found in file'); + Toast.info('No snippets found in file'); return; } @@ -1278,13 +1294,13 @@ function importSnippets(fileInput) { // Save all snippets if (SnippetStorage.saveSnippets(existingSnippets)) { - alert(`Successfully imported ${importedCount} snippet${importedCount !== 1 ? 's' : ''}`); + Toast.success(`Successfully imported ${importedCount} snippet${importedCount !== 1 ? 's' : ''}`); renderSnippetList(); } } catch (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 diff --git a/src/styles.css b/src/styles.css index 97c80c6..012a5c8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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; } .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); } + +/* 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; }