feat: project settings
This commit is contained in:
@@ -261,7 +261,9 @@ to easy template creation.
|
|||||||
For the styling I want the system to be based on css templates, so that the look can be easily swapped
|
For the styling I want the system to be based on css templates, so that the look can be easily swapped
|
||||||
to the wish of the user. There should be a selection of light and dark themes bundled with the application,
|
to the wish of the user. There should be a selection of light and dark themes bundled with the application,
|
||||||
so that starting is simple. New css templates must be easily integrateable into the application,
|
so that starting is simple. New css templates must be easily integrateable into the application,
|
||||||
maybe even with easy importing from a central repository site or something like that.
|
maybe even with easy importing from a central repository site or something like that. I think Pico CSS is a
|
||||||
|
good choice, since it sticks to semantic HTML and auto-adapts to light/dark mode settings of users.
|
||||||
|
I want to work minimalist, but still allow some style influence into the site based on user prefs.
|
||||||
|
|
||||||
Check the site https://hugo.rfc1437.de/ for its structure, this is the structure of blog I want to be
|
Check the site https://hugo.rfc1437.de/ for its structure, this is the structure of blog I want to be
|
||||||
capable of building with this tooling. So we need templates for overview pages and ways to manage menues
|
capable of building with this tooling. So we need templates for overview pages and ways to manage menues
|
||||||
|
|||||||
@@ -185,7 +185,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setting-control input[type="text"],
|
.setting-control input[type="text"],
|
||||||
.setting-control input[type="password"] {
|
.setting-control input[type="password"],
|
||||||
|
.setting-control textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
@@ -195,13 +196,21 @@
|
|||||||
color: var(--vscode-input-foreground);
|
color: var(--vscode-input-foreground);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-control input:focus {
|
.setting-control textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-control input:focus,
|
||||||
|
.setting-control textarea:focus {
|
||||||
border-color: var(--vscode-focusBorder);
|
border-color: var(--vscode-focusBorder);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-control input::placeholder {
|
.setting-control input::placeholder,
|
||||||
|
.setting-control textarea::placeholder {
|
||||||
color: var(--vscode-input-placeholderForeground);
|
color: var(--vscode-input-placeholderForeground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { showToast } from '../Toast';
|
|||||||
import './SettingsView.css';
|
import './SettingsView.css';
|
||||||
|
|
||||||
// Export category IDs for sidebar navigation
|
// Export category IDs for sidebar navigation
|
||||||
export type SettingsCategory = 'editor' | 'content' | 'sync' | 'publishing' | 'data';
|
export type SettingsCategory = 'project' | 'editor' | 'content' | 'sync' | 'publishing' | 'data';
|
||||||
|
|
||||||
// Scroll to a settings section by category ID
|
// Scroll to a settings section by category ID
|
||||||
export const scrollToSettingsSection = (category: SettingsCategory) => {
|
export const scrollToSettingsSection = (category: SettingsCategory) => {
|
||||||
@@ -102,14 +102,18 @@ const SettingSection: React.FC<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsView: React.FC = () => {
|
export const SettingsView: React.FC = () => {
|
||||||
const { preferredEditorMode, setPreferredEditorMode, syncConfigured } = useAppStore();
|
const { preferredEditorMode, setPreferredEditorMode, syncConfigured, activeProject, setActiveProject } = useAppStore();
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [credentials, setCredentials] = useState<Credentials>(defaultCredentials);
|
const [credentials, setCredentials] = useState<Credentials>(defaultCredentials);
|
||||||
const [showSecrets, setShowSecrets] = useState(false);
|
const [showSecrets, setShowSecrets] = useState(false);
|
||||||
const [dropboxConfigured, setDropboxConfigured] = useState(false);
|
const [dropboxConfigured, setDropboxConfigured] = useState(false);
|
||||||
const [dropboxLastSync, setDropboxLastSync] = useState<string | null>(null);
|
const [dropboxLastSync, setDropboxLastSync] = useState<string | null>(null);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Project settings
|
||||||
|
const [projectName, setProjectName] = useState('');
|
||||||
|
const [projectDescription, setProjectDescription] = useState('');
|
||||||
|
|
||||||
// Post categories management
|
// Post categories management
|
||||||
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
|
const [postCategories, setPostCategories] = useState<string[]>(DEFAULT_POST_CATEGORIES);
|
||||||
const [newCategoryInput, setNewCategoryInput] = useState('');
|
const [newCategoryInput, setNewCategoryInput] = useState('');
|
||||||
@@ -128,6 +132,14 @@ export const SettingsView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}, [searchQuery]);
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
// Sync project fields from active project
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeProject) {
|
||||||
|
setProjectName(activeProject.name);
|
||||||
|
setProjectDescription(activeProject.description || '');
|
||||||
|
}
|
||||||
|
}, [activeProject]);
|
||||||
|
|
||||||
// Load saved credentials and categories
|
// Load saved credentials and categories
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
@@ -284,13 +296,76 @@ export const SettingsView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Save project settings
|
||||||
|
const handleSaveProject = async () => {
|
||||||
|
if (!activeProject) return;
|
||||||
|
try {
|
||||||
|
const updated = await window.electronAPI?.projects.update(activeProject.id, {
|
||||||
|
name: projectName.trim() || activeProject.name,
|
||||||
|
description: projectDescription.trim(),
|
||||||
|
});
|
||||||
|
if (updated) {
|
||||||
|
setActiveProject(updated as any);
|
||||||
|
useAppStore.getState().updateProject(activeProject.id, updated as any);
|
||||||
|
}
|
||||||
|
showToast.success('Project settings saved');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save project settings:', error);
|
||||||
|
showToast.error('Failed to save project settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Keywords for each section for search filtering
|
// Keywords for each section for search filtering
|
||||||
|
const projectKeywords = ['project', 'name', 'description', 'blog', 'site'];
|
||||||
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
const editorKeywords = ['editor', 'mode', 'wysiwyg', 'markdown', 'preview', 'visual'];
|
||||||
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
|
const contentKeywords = ['content', 'categories', 'post', 'article', 'picture', 'aside', 'page'];
|
||||||
const syncKeywords = ['sync', 'turso', 'libsql', 'cloud', 'database', 'dropbox', 'file', 'backup', 'token', 'remote'];
|
const syncKeywords = ['sync', 'turso', 'libsql', 'cloud', 'database', 'dropbox', 'file', 'backup', 'token', 'remote'];
|
||||||
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
|
const publishingKeywords = ['publishing', 'ftp', 'ssh', 'deploy', 'server', 'host', 'upload'];
|
||||||
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem'];
|
const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'links', 'folder', 'filesystem'];
|
||||||
|
|
||||||
|
const renderProjectSettings = () => (
|
||||||
|
<SettingSection
|
||||||
|
id="settings-section-project"
|
||||||
|
title="Project"
|
||||||
|
description="General settings for the active blog project."
|
||||||
|
hidden={!sectionHasMatches(projectKeywords)}
|
||||||
|
>
|
||||||
|
<SettingRow
|
||||||
|
id="project-name"
|
||||||
|
label="Project Name"
|
||||||
|
description="The display name of your blog project."
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="project-name"
|
||||||
|
type="text"
|
||||||
|
placeholder="My Blog"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(e) => setProjectName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
|
<SettingRow
|
||||||
|
id="project-description"
|
||||||
|
label="Description"
|
||||||
|
description="A short description of your blog. This can be used in templates and metadata."
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
id="project-description"
|
||||||
|
placeholder="A blog about..."
|
||||||
|
value={projectDescription}
|
||||||
|
onChange={(e) => setProjectDescription(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
|
||||||
|
<div className="setting-actions">
|
||||||
|
<button className="primary" onClick={handleSaveProject}>
|
||||||
|
Save Project Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SettingSection>
|
||||||
|
);
|
||||||
|
|
||||||
const renderEditorSettings = () => (
|
const renderEditorSettings = () => (
|
||||||
<SettingSection
|
<SettingSection
|
||||||
id="settings-section-editor"
|
id="settings-section-editor"
|
||||||
@@ -781,7 +856,8 @@ export const SettingsView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Check if any results match the search
|
// Check if any results match the search
|
||||||
const hasAnyMatches = !searchQuery ||
|
const hasAnyMatches = !searchQuery ||
|
||||||
|
sectionHasMatches(projectKeywords) ||
|
||||||
sectionHasMatches(editorKeywords) ||
|
sectionHasMatches(editorKeywords) ||
|
||||||
sectionHasMatches(contentKeywords) ||
|
sectionHasMatches(contentKeywords) ||
|
||||||
sectionHasMatches(syncKeywords) ||
|
sectionHasMatches(syncKeywords) ||
|
||||||
@@ -816,6 +892,7 @@ export const SettingsView: React.FC = () => {
|
|||||||
<div className="settings-content" ref={contentRef}>
|
<div className="settings-content" ref={contentRef}>
|
||||||
{hasAnyMatches ? (
|
{hasAnyMatches ? (
|
||||||
<>
|
<>
|
||||||
|
{renderProjectSettings()}
|
||||||
{renderEditorSettings()}
|
{renderEditorSettings()}
|
||||||
{renderContentSettings()}
|
{renderContentSettings()}
|
||||||
{renderSyncSettings()}
|
{renderSyncSettings()}
|
||||||
|
|||||||
@@ -634,7 +634,14 @@ const SettingsNav: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-nav-list">
|
<div className="settings-nav-list">
|
||||||
<button
|
<button
|
||||||
|
className={`settings-nav-entry ${activeSection === 'project' ? 'active' : ''}`}
|
||||||
|
onClick={() => handleNavClick('project')}
|
||||||
|
>
|
||||||
|
<span className="settings-nav-entry-icon">📁</span>
|
||||||
|
<span>Project</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
className={`settings-nav-entry ${activeSection === 'editor' ? 'active' : ''}`}
|
className={`settings-nav-entry ${activeSection === 'editor' ? 'active' : ''}`}
|
||||||
onClick={() => handleNavClick('editor')}
|
onClick={() => handleNavClick('editor')}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user