diff --git a/assets/css/style.css b/assets/css/style.css index 91e4239..c337b0e 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -485,20 +485,24 @@ button.secondary:hover { border-color: var(--secondary-color); } -button.danger { +button.danger, +.btn.danger { background: var(--accent-color); } -button.danger:hover { +button.danger:hover, +.btn.danger:hover { background: #c0392b; } -button.success { +button.success, +.btn.success { background: var(--success-color); } -button.success:hover { +button.success:hover, +.btn.success:hover { background: #229954; } @@ -1176,6 +1180,36 @@ select:focus-visible { box-shadow: var(--shadow-hover); } +.journal-item.active { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(41, 128, 185, 0.15); +} + +.journal-item-actions { + display: flex; + align-items: stretch; + gap: 0.5rem; +} + +.journal-item-actions .journal-item { + flex: 1; +} + +.journal-item-actions .journal-delete { + flex-shrink: 0; + padding: 0.6rem 0.9rem; + text-transform: none; +} + +.journal-list.empty { + display: flex; + flex-direction: column; + gap: 1.5rem; + align-items: center; + text-align: center; + color: var(--text-light); +} + .journal-preview { display: flex; flex-direction: column; @@ -1317,4 +1351,4 @@ select:focus-visible { .mt-1 { margin-top: 0.5rem; } .mt-2 { margin-top: 1rem; } -.mt-3 { margin-top: 1.5rem; } \ No newline at end of file +.mt-3 { margin-top: 1.5rem; } diff --git a/assets/js/app.js b/assets/js/app.js index b8c31df..400f834 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -8,6 +8,9 @@ class ConceptionAssistant { this.tocTimer = null; this.isPreviewMode = false; this.originalContent = ''; + this.localBackupKey = 'design-journal-autosave'; + this.localBackupTimer = null; + this.localStorageSupported = this.detectLocalStorageSupport(); this.init(); } @@ -15,6 +18,7 @@ class ConceptionAssistant { this.setupEditor(); this.setupEventListeners(); await this.loadJournalList(); + this.checkForLocalBackup(); this.generateTOC(); this.updateStatistics(); } @@ -26,6 +30,7 @@ class ConceptionAssistant { this.editor.addEventListener('input', () => { this.debounceTOC(); this.updateStatistics(); + this.scheduleLocalBackup(); }); // Keyboard shortcut handling @@ -171,6 +176,134 @@ class ConceptionAssistant { this.redoStack = []; } + detectLocalStorageSupport() { + if (typeof window === 'undefined') { + return false; + } + + try { + const testKey = '__design_journal_test__'; + window.localStorage.setItem(testKey, '1'); + window.localStorage.removeItem(testKey); + return true; + } catch (error) { + console.warn('Local storage unavailable, disabling local backups.', error); + return false; + } + } + + scheduleLocalBackup() { + if (!this.localStorageSupported) return; + + if (this.localBackupTimer) { + clearTimeout(this.localBackupTimer); + } + + this.localBackupTimer = setTimeout(() => { + this.saveLocalBackup(); + }, 1500); + } + + saveLocalBackup(contentOverride = null) { + if (!this.localStorageSupported || typeof window === 'undefined') return; + + try { + const content = typeof contentOverride === 'string' + ? contentOverride + : (this.editor ? this.editor.innerText : ''); + + const normalizedContent = content || ''; + + if (!normalizedContent.trim()) { + window.localStorage.removeItem(this.localBackupKey); + return; + } + + const backupPayload = { + content: normalizedContent, + timestamp: new Date().toISOString(), + journalId: this.currentJournalId + }; + + window.localStorage.setItem(this.localBackupKey, JSON.stringify(backupPayload)); + } catch (error) { + console.warn('Unable to persist local backup.', error); + } + } + + discardLocalBackup() { + if (!this.localStorageSupported || typeof window === 'undefined') return; + try { + window.localStorage.removeItem(this.localBackupKey); + } catch (error) { + console.warn('Unable to discard local backup.', error); + } + } + + checkForLocalBackup() { + if (!this.localStorageSupported || typeof window === 'undefined') return; + + let rawBackup; + try { + rawBackup = window.localStorage.getItem(this.localBackupKey); + } catch (error) { + console.warn('Unable to access local backup.', error); + return; + } + + if (!rawBackup) return; + + let backup; + try { + backup = JSON.parse(rawBackup); + } catch (error) { + console.warn('Invalid local backup payload, clearing it.', error); + this.discardLocalBackup(); + return; + } + + if (!backup || typeof backup.content !== 'string' || !backup.content.trim()) { + this.discardLocalBackup(); + return; + } + + const currentContent = this.editor ? this.editor.innerText : ''; + if (currentContent.trim() === backup.content.trim()) { + return; + } + + const savedAt = backup.timestamp ? new Date(backup.timestamp) : null; + const formattedDate = savedAt && !Number.isNaN(savedAt.getTime()) + ? savedAt.toLocaleString() + : 'a previous session'; + + const shouldRestore = window.confirm( + `A local draft saved on ${formattedDate} was found. Do you want to restore it?` + ); + + if (!shouldRestore) { + this.discardLocalBackup(); + return; + } + + this.editor.innerText = backup.content; + this.currentJournalId = backup.journalId ?? this.currentJournalId; + this.undoStack = [backup.content]; + this.redoStack = []; + this.generateTOC(); + this.updateStatistics(); + this.saveState(true); + this.saveLocalBackup(backup.content); + + const statusEl = document.getElementById('save-status'); + if (statusEl) { + statusEl.textContent = 'Local draft restored'; + setTimeout(() => statusEl.textContent = '', 4000); + } + + this.showNotification('Local draft restored', 'success'); + } + undo() { if (this.undoStack.length > 1) { const currentContent = this.undoStack.pop(); @@ -183,6 +316,7 @@ class ConceptionAssistant { this.generateTOC(); this.showNotification('Undo completed', 'success'); + this.saveLocalBackup(previousContent); } else { this.showNotification('Nothing to undo', 'warning'); } @@ -199,6 +333,7 @@ class ConceptionAssistant { this.generateTOC(); this.showNotification('Redo completed', 'success'); + this.saveLocalBackup(nextContent); } else { this.showNotification('Nothing to redo', 'warning'); } @@ -241,6 +376,8 @@ class ConceptionAssistant { this.currentJournalId = result.data.id; } + this.saveLocalBackup(content); + statusEl.textContent = 'Saved'; this.showNotification('Journal saved successfully', 'success'); setTimeout(() => statusEl.textContent = '', 3000); @@ -282,6 +419,8 @@ class ConceptionAssistant { this.currentJournalId = id; this.editor.innerText = journal.markdownContent; this.generateTOC(); + this.updateStatistics(); + this.saveLocalBackup(journal.markdownContent); // Reset history for the new journal this.undoStack = [journal.markdownContent]; @@ -304,46 +443,12 @@ class ConceptionAssistant { const result = await response.json(); if (result.success) { - const journalList = result.data.map(journal => { - // Extract first lines for preview - const lines = journal.markdownContent.split('\n'); - const firstLines = lines.slice(0, 3).join('\n'); - const preview = firstLines.length > 150 ? firstLines.substring(0, 150) + '...' : firstLines; - - return ` -
No journals saved yet.
+ +