From 93fc9f9cb65ce663a70de5e89342352ddfb961bd Mon Sep 17 00:00:00 2001 From: hugo Date: Tue, 10 Feb 2026 17:16:34 +0100 Subject: [PATCH] fix: category handling --- VISION.md | 8 ++ src/renderer/components/Editor/Editor.tsx | 55 ++++++--- .../components/SettingsView/SettingsView.css | 72 ++++++++++++ .../components/SettingsView/SettingsView.tsx | 109 +++++++++++++++++- 4 files changed, 221 insertions(+), 23 deletions(-) diff --git a/VISION.md b/VISION.md index 775c4bf..a1e283b 100644 --- a/VISION.md +++ b/VISION.md @@ -114,6 +114,14 @@ a gear icon in the bottom of the iconbar. Also login credentials can be managed of the icon bar directly above the gear icon. This is similar to what vscode does, separating logins and settings. +Tags are something that should be mainly focus on reusing but need easy ways to add new tags. This should +not be a simple text field, but more a feature like tags in gitlab, where you can easily select multiple +available tags, but also can quickly create new tags. Tags should have a color and there should be a way +to manage tags in the preferences, too, so that users can create tags upfront to posts. + +Categories are a simple selection via dropdown and a preferences panel that allows category management. +Categories are mainly used to denote different styles of posts and posts are only of one style. + ## Organizing Blog posts should be organized in the app in the main post view where the sidebar lists posts and the main diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index 5168221..321fb72 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -57,7 +57,8 @@ const PostEditor: React.FC = ({ post }) => { const [title, setTitle] = useState(post.title); const [content, setContent] = useState(post.content); const [tags, setTags] = useState(post.tags.join(', ')); - const [categories, setCategories] = useState(post.categories.join(', ')); + const [category, setCategory] = useState(post.categories[0] || 'article'); + const [availableCategories, setAvailableCategories] = useState(['article', 'picture', 'aside', 'page']); const [isSaving, setIsSaving] = useState(false); const [hasPublishedVersion, setHasPublishedVersion] = useState(false); const [editorMode, setEditorMode] = useState(preferredEditorMode); @@ -72,6 +73,21 @@ const PostEditor: React.FC = ({ post }) => { window.electronAPI?.posts.hasPublishedVersion(post.id).then(setHasPublishedVersion); }, [post.id]); + // Load available categories from localStorage + useEffect(() => { + const savedCategories = localStorage.getItem('bds-categories'); + if (savedCategories) { + try { + const parsed = JSON.parse(savedCategories); + if (Array.isArray(parsed) && parsed.length > 0) { + setAvailableCategories(parsed); + } + } catch { + // Keep defaults + } + } + }, []); + // Extract images from content for lightbox const images = useMarkdownImages(content); @@ -80,7 +96,7 @@ const PostEditor: React.FC = ({ post }) => { title: string; content: string; tags: string; - categories: string; + category: string; postId: string; isDirty: boolean; } | null>(null); @@ -91,11 +107,11 @@ const PostEditor: React.FC = ({ post }) => { title, content, tags, - categories, + category, postId: post.id, isDirty, }; - }, [title, content, tags, categories, post.id, isDirty]); + }, [title, content, tags, category, post.id, isDirty]); // Auto-save when switching away from a post or unmounting useEffect(() => { @@ -111,7 +127,7 @@ const PostEditor: React.FC = ({ post }) => { title: pending.title, content: pending.content, tags: pending.tags.split(',').map(t => t.trim()).filter(t => t.length > 0), - categories: pending.categories.split(',').map(c => c.trim()).filter(c => c.length > 0), + categories: pending.category ? [pending.category] : ['article'], }).then((updated) => { if (updated) { useAppStore.getState().updatePost(pending.postId, updated as Partial); @@ -129,24 +145,25 @@ const PostEditor: React.FC = ({ post }) => { setTitle(post.title); setContent(post.content); setTags(post.tags.join(', ')); - setCategories(post.categories.join(', ')); + setCategory(post.categories[0] || 'article'); markClean(post.id); }, [post.id, post.title, post.content, post.tags, post.categories, markClean]); // Track changes useEffect(() => { + const currentCategory = post.categories[0] || 'article'; const hasChanges = title !== post.title || content !== post.content || tags !== post.tags.join(', ') || - categories !== post.categories.join(', '); + category !== currentCategory; if (hasChanges) { markDirty(post.id); } else { markClean(post.id); } - }, [title, content, tags, categories, post, markDirty, markClean]); + }, [title, content, tags, category, post, markDirty, markClean]); // Handle editor mode change and persist preference const handleEditorModeChange = (mode: EditorMode) => { @@ -163,7 +180,7 @@ const PostEditor: React.FC = ({ post }) => { title, content, tags: tags.split(',').map(t => t.trim()).filter(t => t.length > 0), - categories: categories.split(',').map(c => c.trim()).filter(c => c.length > 0), + categories: category ? [category] : ['article'], }); if (updated) { @@ -182,7 +199,7 @@ const PostEditor: React.FC = ({ post }) => { } finally { setIsSaving(false); } - }, [post.id, title, content, tags, categories, isDirty, isSaving, updatePost, markClean, showErrorModal]); + }, [post.id, title, content, tags, category, isDirty, isSaving, updatePost, markClean, showErrorModal]); const handlePublish = async () => { await handleSave(); @@ -240,7 +257,7 @@ const PostEditor: React.FC = ({ post }) => { setTitle(reverted.title); setContent(reverted.content); setTags(reverted.tags.join(', ')); - setCategories(reverted.categories.join(', ')); + setCategory(reverted.categories[0] || 'article'); updatePost(post.id, reverted as Partial); markClean(post.id); showToast.success('Reverted to last published version'); @@ -396,13 +413,15 @@ const PostEditor: React.FC = ({ post }) => { />
- - setCategories(e.target.value)} - placeholder="category1, category2" - /> + +
diff --git a/src/renderer/components/SettingsView/SettingsView.css b/src/renderer/components/SettingsView/SettingsView.css index ee6b3cf..e6ae3e4 100644 --- a/src/renderer/components/SettingsView/SettingsView.css +++ b/src/renderer/components/SettingsView/SettingsView.css @@ -359,6 +359,78 @@ background-color: var(--vscode-button-secondaryHoverBackground); } +/* Categories management styles */ +.categories-list { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding: 12px 16px; +} + +.category-item { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + border-radius: 4px; + font-size: 13px; +} + +.category-name { + font-weight: 500; +} + +.category-remove { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 0; + background: transparent; + border: none; + color: var(--vscode-badge-foreground); + cursor: pointer; + opacity: 0.6; + font-size: 10px; + border-radius: 50%; + transition: opacity 0.15s, background-color 0.15s; +} + +.category-remove:hover { + opacity: 1; + background-color: rgba(255, 255, 255, 0.1); +} + +.category-add-form { + display: flex; + gap: 8px; + padding: 12px 16px; + align-items: center; +} + +.category-add-form input { + flex: 1; + max-width: 300px; + padding: 6px 12px; + font-size: 13px; + background-color: var(--vscode-input-background); + border: 1px solid var(--vscode-input-border); + color: var(--vscode-input-foreground); + border-radius: 4px; + outline: none; +} + +.category-add-form input:focus { + border-color: var(--vscode-focusBorder); +} + +.category-add-form input::placeholder { + color: var(--vscode-input-placeholderForeground); +} + /* Responsive - narrow sidebar */ @media (max-width: 600px) { .settings-nav { diff --git a/src/renderer/components/SettingsView/SettingsView.tsx b/src/renderer/components/SettingsView/SettingsView.tsx index 5bcd100..5e34461 100644 --- a/src/renderer/components/SettingsView/SettingsView.tsx +++ b/src/renderer/components/SettingsView/SettingsView.tsx @@ -4,7 +4,7 @@ import { showToast } from '../Toast'; import './SettingsView.css'; // Settings categories matching VS Code style -type SettingsCategory = 'editor' | 'sync' | 'publishing' | 'data'; +type SettingsCategory = 'editor' | 'content' | 'sync' | 'publishing' | 'data'; interface Credentials { // Turso Cloud Sync @@ -45,9 +45,13 @@ const SearchIcon = () => ( ); +// Default post categories based on VISION.md +const DEFAULT_POST_CATEGORIES = ['article', 'picture', 'aside', 'page']; + // Category definitions const categories: { id: SettingsCategory; label: string; icon: string }[] = [ { id: 'editor', label: 'Editor', icon: '📝' }, + { id: 'content', label: 'Content', icon: '📋' }, { id: 'sync', label: 'Sync', icon: '🔄' }, { id: 'publishing', label: 'Publishing', icon: '🚀' }, { id: 'data', label: 'Data Management', icon: '🗄️' }, @@ -96,16 +100,29 @@ export const SettingsView: React.FC = () => { const [showSecrets, setShowSecrets] = useState(false); const [dropboxConfigured, setDropboxConfigured] = useState(false); const [dropboxLastSync, setDropboxLastSync] = useState(null); + + // Post categories management + const [postCategories, setPostCategories] = useState(DEFAULT_POST_CATEGORIES); + const [newCategoryInput, setNewCategoryInput] = useState(''); - // Load saved credentials + // Load saved credentials and categories useEffect(() => { - const loadCredentials = async () => { + const loadSettings = async () => { try { const savedCreds = localStorage.getItem('bds-credentials'); if (savedCreds) { setCredentials({ ...defaultCredentials, ...JSON.parse(savedCreds) }); } + // Load saved post categories + const savedCategories = localStorage.getItem('bds-categories'); + if (savedCategories) { + const categories = JSON.parse(savedCategories); + if (Array.isArray(categories) && categories.length > 0) { + setPostCategories(categories); + } + } + // Check Dropbox status const dbxConfigured = await window.electronAPI?.dropbox?.isConfigured(); setDropboxConfigured(dbxConfigured || false); @@ -115,10 +132,10 @@ export const SettingsView: React.FC = () => { setDropboxLastSync(lastSync || null); } } catch (error) { - console.error('Failed to load credentials:', error); + console.error('Failed to load settings:', error); } }; - loadCredentials(); + loadSettings(); }, []); // Save credentials and configure backends @@ -274,6 +291,85 @@ export const SettingsView: React.FC = () => { ); + // Handlers for post categories management + const handleAddCategory = () => { + const trimmed = newCategoryInput.trim().toLowerCase(); + if (trimmed && !postCategories.includes(trimmed)) { + const updated = [...postCategories, trimmed]; + setPostCategories(updated); + localStorage.setItem('bds-categories', JSON.stringify(updated)); + setNewCategoryInput(''); + showToast.success(`Category "${trimmed}" added`); + } else if (postCategories.includes(trimmed)) { + showToast.error('Category already exists'); + } + }; + + const handleRemoveCategory = (categoryToRemove: string) => { + if (postCategories.length <= 1) { + showToast.error('Must have at least one category'); + return; + } + const updated = postCategories.filter(c => c !== categoryToRemove); + setPostCategories(updated); + localStorage.setItem('bds-categories', JSON.stringify(updated)); + showToast.success(`Category "${categoryToRemove}" removed`); + }; + + const handleResetCategories = () => { + setPostCategories(DEFAULT_POST_CATEGORIES); + localStorage.setItem('bds-categories', JSON.stringify(DEFAULT_POST_CATEGORIES)); + showToast.success('Categories reset to defaults'); + }; + + const renderContentSettings = () => ( + <> + +
+ {postCategories.map((cat) => ( +
+ {cat} + +
+ ))} +
+ +
+ setNewCategoryInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleAddCategory(); + } + }} + /> + +
+ +
+ +
+
+ + ); + const renderSyncSettings = () => ( <> { return ( <> {renderEditorSettings()} + {renderContentSettings()} {renderSyncSettings()} {renderPublishingSettings()} {renderDataSettings()} @@ -657,6 +754,8 @@ export const SettingsView: React.FC = () => { switch (activeCategory) { case 'editor': return renderEditorSettings(); + case 'content': + return renderContentSettings(); case 'sync': return renderSyncSettings(); case 'publishing':