fix: category handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: '🗄️' },
|
||||
@@ -97,15 +101,28 @@ export const SettingsView: React.FC = () => {
|
||||
const [dropboxConfigured, setDropboxConfigured] = useState(false);
|
||||
const [dropboxLastSync, setDropboxLastSync] = useState<string | null>(null);
|
||||
|
||||
// Load saved credentials
|
||||
// Post categories management
|
||||
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
|
||||
const [newCategoryInput, setNewCategoryInput] = useState('');
|
||||
|
||||
// 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':
|
||||
|
||||
Reference in New Issue
Block a user