@@ -498,7 +874,7 @@ const WelcomeScreen: React.FC = () => {
Create a New Post
Start writing your next blog post with Markdown support.
-
@@ -545,18 +921,60 @@ const WelcomeScreen: React.FC = () => {
};
export const Editor: React.FC = () => {
- const { activeView, selectedPostId, selectedMediaId, posts } = useAppStore();
+ const {
+ activeView,
+ selectedPostId,
+ selectedMediaId,
+ posts,
+ unsavedDrafts,
+ errorModal,
+ hideErrorModal,
+ } = useAppStore();
+
+ // Show error modal if present
+ const renderErrorModal = () => (
+
+ );
if (activeView === 'posts' && selectedPostId) {
+ // Check if it's an unsaved draft
+ if (isUnsavedDraftId(selectedPostId)) {
+ const draft = unsavedDrafts.find(d => d.id === selectedPostId);
+ if (draft) {
+ return (
+ <>
+
+ {renderErrorModal()}
+ >
+ );
+ }
+ }
+
+ // Otherwise, it's a saved post
const post = posts.find(p => p.id === selectedPostId);
if (post) {
- return
;
+ return (
+ <>
+
+ {renderErrorModal()}
+ >
+ );
}
}
if (activeView === 'media' && selectedMediaId) {
- return
;
+ return (
+ <>
+
+ {renderErrorModal()}
+ >
+ );
}
- return
;
+ return (
+ <>
+
+ {renderErrorModal()}
+ >
+ );
};
diff --git a/src/renderer/components/ErrorModal/ErrorModal.css b/src/renderer/components/ErrorModal/ErrorModal.css
new file mode 100644
index 0000000..8f234a1
--- /dev/null
+++ b/src/renderer/components/ErrorModal/ErrorModal.css
@@ -0,0 +1,135 @@
+.error-modal-backdrop {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.6);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+}
+
+.error-modal {
+ background: var(--color-bg-secondary, #1e1e1e);
+ border: 1px solid var(--color-border, #3c3c3c);
+ border-radius: 8px;
+ min-width: 400px;
+ max-width: 700px;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+}
+
+.error-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--color-border, #3c3c3c);
+}
+
+.error-modal-header h2 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--color-error, #f14c4c);
+}
+
+.error-modal-close {
+ background: none;
+ border: none;
+ color: var(--color-text-muted, #888);
+ cursor: pointer;
+ font-size: 18px;
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+.error-modal-close:hover {
+ background: var(--color-bg-tertiary, #2a2a2a);
+ color: var(--color-text, #fff);
+}
+
+.error-modal-body {
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.error-message {
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--color-text, #ccc);
+ margin-bottom: 16px;
+}
+
+.error-stack-section {
+ margin-top: 12px;
+}
+
+.error-stack-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.error-stack-header span {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-text-muted, #888);
+ text-transform: uppercase;
+}
+
+.copy-button {
+ background: var(--color-bg-tertiary, #2a2a2a);
+ border: 1px solid var(--color-border, #3c3c3c);
+ color: var(--color-text, #ccc);
+ padding: 4px 10px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+}
+
+.copy-button:hover {
+ background: var(--color-bg-hover, #333);
+}
+
+.error-stack {
+ background: var(--color-bg-primary, #0d0d0d);
+ border: 1px solid var(--color-border, #3c3c3c);
+ border-radius: 4px;
+ padding: 12px;
+ font-family: 'Cascadia Code', 'Consolas', 'Courier New', monospace;
+ font-size: 12px;
+ color: var(--color-text-muted, #888);
+ overflow-x: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ max-height: 300px;
+ margin: 0;
+}
+
+.error-modal-footer {
+ display: flex;
+ justify-content: flex-end;
+ padding: 16px 20px;
+ border-top: 1px solid var(--color-border, #3c3c3c);
+}
+
+.error-modal-footer button {
+ background: var(--color-primary, #0e639c);
+ border: none;
+ color: #fff;
+ padding: 8px 20px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 13px;
+}
+
+.error-modal-footer button:hover {
+ background: var(--color-primary-hover, #1177bb);
+}
diff --git a/src/renderer/components/ErrorModal/ErrorModal.tsx b/src/renderer/components/ErrorModal/ErrorModal.tsx
new file mode 100644
index 0000000..4979a4e
--- /dev/null
+++ b/src/renderer/components/ErrorModal/ErrorModal.tsx
@@ -0,0 +1,62 @@
+import React, { useCallback } from 'react';
+import './ErrorModal.css';
+
+export interface ErrorDetails {
+ message: string;
+ title?: string;
+ stack?: string;
+}
+
+interface ErrorModalProps {
+ error: ErrorDetails | null;
+ onClose: () => void;
+}
+
+export const ErrorModal: React.FC
= ({ error, onClose }) => {
+ if (!error) return null;
+
+ const handleCopyStack = useCallback(async () => {
+ const textToCopy = `${error.title || 'Error'}\n${error.message}\n\nStack Trace:\n${error.stack || 'No stack trace available'}`;
+ try {
+ await navigator.clipboard.writeText(textToCopy);
+ } catch (err) {
+ console.error('Failed to copy to clipboard:', err);
+ }
+ }, [error]);
+
+ const handleBackdropClick = useCallback((e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) {
+ onClose();
+ }
+ }, [onClose]);
+
+ return (
+
+
+
+
{error.title || 'Error'}
+
+ ✕
+
+
+
+
{error.message}
+ {error.stack && (
+
+
+ Stack Trace
+
+ 📋 Copy
+
+
+
{error.stack}
+
+ )}
+
+
+ Close
+
+
+
+ );
+};
diff --git a/src/renderer/components/ErrorModal/index.ts b/src/renderer/components/ErrorModal/index.ts
new file mode 100644
index 0000000..bfb6c74
--- /dev/null
+++ b/src/renderer/components/ErrorModal/index.ts
@@ -0,0 +1,2 @@
+export { ErrorModal } from './ErrorModal';
+export type { ErrorDetails } from './ErrorModal';
diff --git a/src/renderer/components/Sidebar/Sidebar.css b/src/renderer/components/Sidebar/Sidebar.css
index d92f20f..7559ea1 100644
--- a/src/renderer/components/Sidebar/Sidebar.css
+++ b/src/renderer/components/Sidebar/Sidebar.css
@@ -495,3 +495,28 @@
.filter-status button:hover {
text-decoration: underline;
}
+
+/* Unsaved drafts styling */
+.status-unsaved {
+ color: #f59e0b;
+}
+
+.sidebar-item.unsaved {
+ background: linear-gradient(90deg, rgba(245, 158, 11, 0.1) 0%, transparent 100%);
+}
+
+.sidebar-item.unsaved .sidebar-item-title {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.unsaved-indicator {
+ color: #f59e0b;
+ font-size: 10px;
+ flex-shrink: 0;
+}
+
+.sidebar-item.unsaved .sidebar-item-meta {
+ font-style: italic;
+}
diff --git a/src/renderer/components/Sidebar/Sidebar.tsx b/src/renderer/components/Sidebar/Sidebar.tsx
index 6fd101d..c01f1ad 100644
--- a/src/renderer/components/Sidebar/Sidebar.tsx
+++ b/src/renderer/components/Sidebar/Sidebar.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { useAppStore, PostData } from '../../store';
+import { useAppStore, PostData, UnsavedDraft } from '../../store';
import { showToast } from '../Toast';
import './Sidebar.css';
@@ -222,7 +222,7 @@ const SearchBox: React.FC = ({ onSearch }) => {
};
const PostsList: React.FC = () => {
- const { posts, selectedPostId, setSelectedPost } = useAppStore();
+ const { posts, selectedPostId, setSelectedPost, unsavedDrafts } = useAppStore();
// Filter state
const [searchQuery, setSearchQuery] = useState('');
@@ -321,20 +321,11 @@ const PostsList: React.FC = () => {
applyFilters();
}, [selectedTags, selectedCategories]);
- const handleCreatePost = async () => {
- try {
- const newPost = await window.electronAPI?.posts.create({
- title: 'Untitled Post',
- content: '# New Post\n\nStart writing your content here...',
- });
- if (newPost) {
- setSelectedPost((newPost as PostData).id);
- showToast.success('Post created');
- }
- } catch (error) {
- console.error('Failed to create post:', error);
- showToast.error('Failed to create post');
- }
+ const handleCreatePost = () => {
+ // Create an unsaved draft instead of immediately saving to database
+ const { createUnsavedDraft, setSelectedPost: selectPost } = useAppStore.getState();
+ const draftId = createUnsavedDraft();
+ selectPost(draftId);
};
// Determine which posts to display
@@ -414,6 +405,34 @@ const PostsList: React.FC = () => {
)}
+ {/* Unsaved Drafts Section - always show at top if there are any */}
+ {unsavedDrafts.length > 0 && (
+