fix: category handling

This commit is contained in:
2026-02-10 17:16:34 +01:00
parent 29d9308100
commit 93fc9f9cb6
4 changed files with 221 additions and 23 deletions

View File

@@ -57,7 +57,8 @@ const PostEditor: React.FC<PostEditorProps> = ({ 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<string[]>(['article', 'picture', 'aside', 'page']);
const [isSaving, setIsSaving] = useState(false);
const [hasPublishedVersion, setHasPublishedVersion] = useState(false);
const [editorMode, setEditorMode] = useState<EditorMode>(preferredEditorMode);
@@ -72,6 +73,21 @@ const PostEditor: React.FC<PostEditorProps> = ({ 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<PostEditorProps> = ({ 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<PostEditorProps> = ({ 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<PostEditorProps> = ({ 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<PostData>);
@@ -129,24 +145,25 @@ const PostEditor: React.FC<PostEditorProps> = ({ 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<PostEditorProps> = ({ 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<PostEditorProps> = ({ 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<PostEditorProps> = ({ 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<PostData>);
markClean(post.id);
showToast.success('Reverted to last published version');
@@ -396,13 +413,15 @@ const PostEditor: React.FC<PostEditorProps> = ({ post }) => {
/>
</div>
<div className="editor-field">
<label>Categories (comma-separated)</label>
<input
type="text"
value={categories}
onChange={(e) => setCategories(e.target.value)}
placeholder="category1, category2"
/>
<label>Category</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
>
{availableCategories.map((cat) => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
</div>

View File

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

View File

@@ -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 = () => (
</svg>
);
// 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<string | null>(null);
// Post categories management
const [postCategories, setPostCategories] = useState<string[]>(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 = () => (
<>
<SettingSection
title="Post Categories"
description="Manage the available categories for blog posts. Each post can have one category that determines its display template."
>
<div className="categories-list">
{postCategories.map((cat) => (
<div key={cat} className="category-item">
<span className="category-name">{cat}</span>
<button
className="category-remove"
onClick={() => handleRemoveCategory(cat)}
title={`Remove "${cat}" category`}
>
</button>
</div>
))}
</div>
<div className="category-add-form">
<input
type="text"
placeholder="New category name..."
value={newCategoryInput}
onChange={(e) => setNewCategoryInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCategory();
}
}}
/>
<button className="primary" onClick={handleAddCategory}>
Add Category
</button>
</div>
<div className="setting-actions">
<button className="secondary" onClick={handleResetCategories}>
Reset to Defaults
</button>
</div>
</SettingSection>
</>
);
const renderSyncSettings = () => (
<>
<SettingSection
@@ -647,6 +743,7 @@ export const SettingsView: React.FC = () => {
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':