Răsfoiți Sursa

theme decoupling

Adam Jafarov 3 săptămâni în urmă
părinte
comite
d4af9c05c8

+ 82 - 0
backend/config.js

@@ -0,0 +1,82 @@
+import fs from 'fs-extra';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const CONFIG_FILE = path.join(__dirname, 'config.json');
+
+// Default config
+const DEFAULT_CONFIG = {
+    activeTheme: 'default',
+    postWidth: 'max-w-4xl'
+};
+
+// Initialize config file
+async function initializeConfig() {
+    try {
+        if (!(await fs.pathExists(CONFIG_FILE))) {
+            await fs.writeJSON(CONFIG_FILE, DEFAULT_CONFIG, { spaces: 2 });
+            console.log('⚙️ Initialized config system');
+        }
+    } catch (error) {
+        console.error('Error initializing config:', error);
+    }
+}
+
+// Load config data
+async function loadConfig() {
+    try {
+        if (await fs.pathExists(CONFIG_FILE)) {
+            return await fs.readJSON(CONFIG_FILE);
+        }
+        return DEFAULT_CONFIG;
+    } catch (error) {
+        console.error('Error loading config:', error);
+        return DEFAULT_CONFIG;
+    }
+}
+
+// Save config data
+async function saveConfig(configData) {
+    try {
+        await fs.writeJSON(CONFIG_FILE, configData, { spaces: 2 });
+    } catch (error) {
+        console.error('Error saving config:', error);
+        throw error;
+    }
+}
+
+// Initialize on load
+await initializeConfig();
+
+// Config getters/setters
+export async function getConfig() {
+    return await loadConfig();
+}
+
+export async function updateConfig(updates) {
+    const currentConfig = await loadConfig();
+    const newConfig = { ...currentConfig, ...updates };
+    await saveConfig(newConfig);
+    return newConfig;
+}
+
+export async function getActiveThemeId() {
+    const config = await loadConfig();
+    return config.activeTheme;
+}
+
+export async function setActiveThemeId(themeId) {
+    return await updateConfig({ activeTheme: themeId });
+}
+
+export async function getPostWidth() {
+    const config = await loadConfig();
+    return config.postWidth;
+}
+
+export async function setPostWidth(width) {
+    return await updateConfig({ postWidth: width });
+}

+ 4 - 0
backend/config.json

@@ -0,0 +1,4 @@
+{
+  "activeTheme": "ocean",
+  "postWidth": "max-w-6xl"
+}

+ 62 - 0
backend/server.js

@@ -634,6 +634,34 @@ app.put("/api/posts/:slug", requireAuth, async (req, res) => {
     }
 });
 
+// Change password endpoint
+app.post('/api/auth/change-password', requireAuth, async (req, res) => {
+    try {
+        const { currentPassword, newPassword } = req.body;
+
+        if (!currentPassword || !newPassword) {
+            return res.status(400).json({ error: 'Current and new password are required' });
+        }
+
+        if (newPassword.length < 6) {
+            return res.status(400).json({ error: 'New password must be at least 6 characters' });
+        }
+
+        const result = await changeUserPassword(req.user.username, currentPassword, newPassword);
+
+        if (!result.success) {
+            return res.status(400).json({ error: result.message });
+        }
+
+        console.log(`Password changed for user: ${req.user.username}`);
+        res.json({ success: true, message: 'Password changed successfully' });
+
+    } catch (err) {
+        console.error('Change password error:', err);
+        res.status(500).json({ error: 'Internal server error' });
+    }
+});
+
 // DELETE /api/posts/:slug - Delete post
 app.delete("/api/posts/:slug", requireAuth, async (req, res) => {
     try {
@@ -709,6 +737,40 @@ app.put("/api/themes/active", requireAuth, async (req, res) => {
     }
 });
 
+import { getConfig, updateConfig } from './config.js';
+
+// GET /api/config - Get app configuration
+app.get("/api/config", async (req, res) => {
+    try {
+        const config = await getConfig();
+        res.json(config);
+    } catch (error) {
+        console.error("Error getting config:", error);
+        res.status(500).json({ error: "Failed to get config" });
+    }
+});
+
+// PUT /api/config - Update app configuration
+app.put("/api/config", requireAuth, async (req, res) => {
+    try {
+        const updates = req.body;
+        // Whitelist allowed config keys to prevent abuse
+        const allowedKeys = ['postWidth', 'activeTheme'];
+        const filteredUpdates = Object.keys(updates)
+            .filter(key => allowedKeys.includes(key))
+            .reduce((obj, key) => {
+                obj[key] = updates[key];
+                return obj;
+            }, {});
+
+        const newConfig = await updateConfig(filteredUpdates);
+        res.json({ success: true, config: newConfig });
+    } catch (error) {
+        console.error("Error updating config:", error);
+        res.status(500).json({ error: "Failed to update config" });
+    }
+});
+
 // POST /api/themes - Create custom theme
 app.post("/api/themes", requireAuth, async (req, res) => {
     try {

+ 33 - 24
backend/themes.js

@@ -30,7 +30,7 @@ const DEFAULT_THEME = {
     headingFontFamily: 'Inter, system-ui, sans-serif',
     fontSize: {
       xs: '0.75rem',
-      sm: '0.875rem', 
+      sm: '0.875rem',
       base: '1rem',
       lg: '1.125rem',
       xl: '1.25rem',
@@ -48,7 +48,7 @@ const DEFAULT_THEME = {
   layout: {
     borderRadius: {
       sm: '0.125rem',
-      base: '0.25rem', 
+      base: '0.25rem',
       md: '0.375rem',
       lg: '0.5rem',
       xl: '0.75rem',
@@ -179,11 +179,19 @@ async function saveThemes(themesData) {
   }
 }
 
+import { getActiveThemeId, setActiveThemeId } from './config.js';
+
+// ... existing imports ...
+
+// ... (keep constants and loadThemes/saveThemes as is, but remove activeTheme from their logic if possible, 
+// strictly speaking we just need to ensure getActiveTheme/setActiveTheme use the new system)
+
 // Get all themes (built-in + custom)
 export async function getAllThemes() {
   const data = await loadThemes();
+  const activeThemeId = await getActiveThemeId();
   return {
-    activeTheme: data.activeTheme,
+    activeTheme: activeThemeId,
     themes: [...data.builtInThemes, ...data.customThemes]
   };
 }
@@ -191,69 +199,69 @@ export async function getAllThemes() {
 // Get active theme
 export async function getActiveTheme() {
   const data = await loadThemes();
+  const activeThemeId = await getActiveThemeId();
   const allThemes = [...data.builtInThemes, ...data.customThemes];
-  return allThemes.find(theme => theme.id === data.activeTheme) || DEFAULT_THEME;
+  return allThemes.find(theme => theme.id === activeThemeId) || DEFAULT_THEME;
 }
 
 // Set active theme
 export async function setActiveTheme(themeId) {
   const data = await loadThemes();
   const allThemes = [...data.builtInThemes, ...data.customThemes];
-  
+
   if (!allThemes.find(theme => theme.id === themeId)) {
     throw new Error('Theme not found');
   }
-  
-  data.activeTheme = themeId;
-  await saveThemes(data);
-  
+
+  await setActiveThemeId(themeId);
+
   return allThemes.find(theme => theme.id === themeId);
 }
 
 // Create custom theme
 export async function createCustomTheme(themeData) {
   const data = await loadThemes();
-  
+
   // Validate required fields
   if (!themeData.name || !themeData.id) {
     throw new Error('Theme name and id are required');
   }
-  
+
   // Check if theme ID already exists
   const allThemes = [...data.builtInThemes, ...data.customThemes];
   if (allThemes.find(theme => theme.id === themeData.id)) {
     throw new Error('Theme ID already exists');
   }
-  
+
   const newTheme = {
     ...DEFAULT_THEME,
     ...themeData,
     createdAt: new Date().toISOString(),
     isBuiltIn: false
   };
-  
+
   data.customThemes.push(newTheme);
   await saveThemes(data);
-  
+
   return newTheme;
 }
 
 // Update custom theme
 export async function updateCustomTheme(themeId, themeData) {
   const data = await loadThemes();
-  
+
   const themeIndex = data.customThemes.findIndex(theme => theme.id === themeId);
   if (themeIndex === -1) {
     throw new Error('Custom theme not found');
   }
-  
+
   data.customThemes[themeIndex] = {
     ...data.customThemes[themeIndex],
     ...themeData,
     id: themeId, // Prevent ID changes
     updatedAt: new Date().toISOString()
   };
-  
+
   await saveThemes(data);
   return data.customThemes[themeIndex];
 }
@@ -261,17 +269,18 @@ export async function updateCustomTheme(themeId, themeData) {
 // Delete custom theme
 export async function deleteCustomTheme(themeId) {
   const data = await loadThemes();
-  
+
   const themeIndex = data.customThemes.findIndex(theme => theme.id === themeId);
   if (themeIndex === -1) {
     throw new Error('Custom theme not found');
   }
-  
+
   // If this was the active theme, switch to default
-  if (data.activeTheme === themeId) {
-    data.activeTheme = 'default';
+  const activeThemeId = await getActiveThemeId();
+  if (activeThemeId === themeId) {
+    await setActiveThemeId('default');
   }
-  
+
   data.customThemes.splice(themeIndex, 1);
   await saveThemes(data);
 }
@@ -281,11 +290,11 @@ export async function exportTheme(themeId) {
   const data = await loadThemes();
   const allThemes = [...data.builtInThemes, ...data.customThemes];
   const theme = allThemes.find(theme => theme.id === themeId);
-  
+
   if (!theme) {
     throw new Error('Theme not found');
   }
-  
+
   // Remove internal fields for export
   const { createdAt, updatedAt, isBuiltIn, ...exportData } = theme;
   return exportData;

+ 1 - 1
public/posts

@@ -1 +1 @@
-Subproject commit 1e0107d99137ab091dd7f1efb5d995fdd8f827cf
+Subproject commit c044583471bad933b17f65372dfaa23dfc1011ed

+ 107 - 0
src/components/AdminDashboard.jsx

@@ -4,6 +4,101 @@ import { useTheme } from "../contexts/ThemeContext";
 
 import { API_BASE } from "../config";
 
+function ChangePasswordForm() {
+    const [currentPassword, setCurrentPassword] = useState("");
+    const [newPassword, setNewPassword] = useState("");
+    const [confirmPassword, setConfirmPassword] = useState("");
+    const [message, setMessage] = useState(null);
+    const [error, setError] = useState(null);
+    const [loading, setLoading] = useState(false);
+
+    const handleSubmit = async (e) => {
+        e.preventDefault();
+        setMessage(null);
+        setError(null);
+
+        if (newPassword !== confirmPassword) {
+            setError("New passwords do not match");
+            return;
+        }
+
+        if (newPassword.length < 6) {
+            setError("Password must be at least 6 characters");
+            return;
+        }
+
+        try {
+            setLoading(true);
+            const response = await fetch(`${API_BASE}/api/auth/change-password`, {
+                method: "POST",
+                headers: { "Content-Type": "application/json" },
+                body: JSON.stringify({ currentPassword, newPassword }),
+                credentials: "include",
+            });
+
+            const data = await response.json();
+
+            if (!response.ok) throw new Error(data.error || "Failed to change password");
+
+            setMessage(data.message);
+            setCurrentPassword("");
+            setNewPassword("");
+            setConfirmPassword("");
+        } catch (err) {
+            setError(err.message);
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    return (
+        <form onSubmit={handleSubmit} className="space-y-4 p-4 border theme-border rounded bg-white dark:bg-gray-700">
+            <h3 className="font-semibold theme-text">Change Password</h3>
+            {message && <div className="text-green-600 text-sm bg-green-50 p-2 rounded">{message}</div>}
+            {error && <div className="text-red-600 text-sm bg-red-50 p-2 rounded">{error}</div>}
+
+            <div>
+                <label className="block text-sm font-medium theme-text-secondary mb-1">Current Password</label>
+                <input
+                    type="password"
+                    value={currentPassword}
+                    onChange={(e) => setCurrentPassword(e.target.value)}
+                    required
+                    className="w-full px-3 py-2 border rounded-md dark:bg-gray-800 theme-text dark:border-gray-600 focus:ring-2 focus:ring-blue-500"
+                />
+            </div>
+            <div>
+                <label className="block text-sm font-medium theme-text-secondary mb-1">New Password</label>
+                <input
+                    type="password"
+                    value={newPassword}
+                    onChange={(e) => setNewPassword(e.target.value)}
+                    required
+                    minLength={6}
+                    className="w-full px-3 py-2 border rounded-md dark:bg-gray-800 theme-text dark:border-gray-600 focus:ring-2 focus:ring-blue-500"
+                />
+            </div>
+            <div>
+                <label className="block text-sm font-medium theme-text-secondary mb-1">Confirm New Password</label>
+                <input
+                    type="password"
+                    value={confirmPassword}
+                    onChange={(e) => setConfirmPassword(e.target.value)}
+                    required
+                    className="w-full px-3 py-2 border rounded-md dark:bg-gray-800 theme-text dark:border-gray-600 focus:ring-2 focus:ring-blue-500"
+                />
+            </div>
+            <button
+                type="submit"
+                disabled={loading}
+                className="w-full btn-theme-primary text-white py-2 rounded-md font-medium disabled:opacity-50"
+            >
+                {loading ? "Updating..." : "Update Password"}
+            </button>
+        </form>
+    );
+}
+
 function AdminDashboard() {
     const [posts, setPosts] = useState([]);
     const [loading, setLoading] = useState(true);
@@ -121,6 +216,18 @@ function AdminDashboard() {
                             </Link>
                         </div>
                     </div>
+                    {/* Inline Password Change Toggle */}
+                    <div className="px-6 py-2 bg-gray-50 dark:bg-gray-800 border-t theme-border">
+                        <details className="group">
+                            <summary className="cursor-pointer text-sm font-medium theme-text-secondary hover:theme-text list-none flex items-center">
+                                <span className="mr-2">🔐 Security Settings</span>
+                                <svg className="w-4 h-4 transform group-open:rotate-180 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg>
+                            </summary>
+                            <div className="mt-4 max-w-md pb-4">
+                                <ChangePasswordForm />
+                            </div>
+                        </details>
+                    </div>
                 </div>
 
                 {/* Stats */}

+ 15 - 9
src/components/Layout.jsx

@@ -66,17 +66,23 @@ export function NavHeader() {
     );
 }
 
+import { useTheme } from '../contexts/ThemeContext';
+
 // Layout ComponentWrapper
-const Layout = ({ children }) => (
-    <div className="min-h-screen theme-bg font-sans theme-text antialiased flex flex-col">
-        <div className="max-w-5xl mx-auto w-full flex-grow">
-            <NavHeader />
-            <main className="py-10 px-4 sm:px-6 lg:px-8">
-                {children}
-            </main>
+const Layout = ({ children }) => {
+    const { postWidth } = useTheme();
+
+    return (
+        <div className="min-h-screen theme-bg font-sans theme-text antialiased flex flex-col">
+            <div className={`${postWidth || 'max-w-4xl'} mx-auto w-full flex-grow transition-all duration-300`}>
+                <NavHeader />
+                <main className="py-10 px-4 sm:px-6 lg:px-8">
+                    {children}
+                </main>
+            </div>
         </div>
-    </div>
-);
+    );
+};
 
 // Skeleton Components
 export const SkeletonCard = () => (

+ 10 - 23
src/components/MediaGalleryModal.jsx

@@ -84,12 +84,12 @@ function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
 
     return (
         <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
-            <div className="bg-white rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col overflow-hidden">
+            <div className="theme-surface rounded-xl shadow-2xl w-full max-w-4xl max-h-[80vh] flex flex-col overflow-hidden theme-border border">
                 {/* Header */}
-                <div className="px-6 py-4 border-b flex justify-between items-center bg-gray-50">
-                    <h3 className="text-lg font-bold text-gray-800">Media Gallery</h3>
+                <div className="px-6 py-4 border-b theme-border flex justify-between items-center bg-gray-50/50">
+                    <h3 className="text-lg font-bold theme-text">Media Gallery</h3>
                     <div className="flex items-center space-x-4">
-                        <label className="cursor-pointer bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium">
+                        <label className="cursor-pointer btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors text-sm font-medium">
                             {uploading ? "Uploading..." : "Upload Image"}
                             <input
                                 type="file"
@@ -101,7 +101,7 @@ function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
                         </label>
                         <button
                             onClick={onClose}
-                            className="text-gray-500 hover:text-gray-700"
+                            className="text-gray-500 hover:text-gray-700 theme-text-secondary hover:theme-text"
                         >
                             <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
                         </button>
@@ -109,7 +109,7 @@ function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
                 </div>
 
                 {/* Content */}
-                <div className="p-6 overflow-y-auto flex-grow bg-gray-100">
+                <div className="p-6 overflow-y-auto flex-grow theme-bg">
                     {error && (
                         <div className="mb-4 p-3 bg-red-100 text-red-700 rounded-lg">
                             {error}
@@ -121,7 +121,7 @@ function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
                             <div className="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent"></div>
                         </div>
                     ) : images.length === 0 ? (
-                        <div className="text-center py-12 text-gray-500">
+                        <div className="text-center py-12 theme-text-secondary">
                             No images found in this folder.
                         </div>
                     ) : (
@@ -129,21 +129,8 @@ function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
                             {images.map((img) => (
                                 <div
                                     key={img.name}
-                                    className="group relative bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow cursor-pointer aspect-square"
-                                    onClick={() => onSelect(`${API_BASE}${img.url}`, img.name)} // Pass full URL for valid backend access? 
-                                // Actually, if we serve static files via /posts, and API_BASE is e.g. localhost:3001, we want the proxy handling or direct URL.
-                                // But markdown needs to work in PROD.
-                                // If we use relative URL `/api/posts/...` no.
-                                // We exposed `/posts` statically in server.js.
-                                // So access is `http://localhost:3001/posts/...`
-                                // Ideally we return absolute URL or relative to root if frontend acts as proxy.
-                                // Currently frontend (Vite) proxies /api.
-                                // We need to proxy /posts too or use full URL.
-                                // Let's use full URL for safety or decide based on config.
-                                // Better: Return relative path `/api/posts/...` ? No, `server.js` serves at `/posts`.
-                                // So `img.url` is likely `/posts/...` from the API response logic.
-                                // See server.js: `urlPath = /posts/...`
-                                // So we just need `API_BASE + img.url` IF `API_BASE` points to backend.
+                                    className="group relative theme-surface rounded-lg shadow-sm border theme-border overflow-hidden hover:shadow-md transition-shadow cursor-pointer aspect-square"
+                                    onClick={() => onSelect(`${API_BASE}${img.url}`, img.name)}
                                 >
                                     <img
                                         src={`${API_BASE}${img.url}`}
@@ -172,7 +159,7 @@ function MediaGalleryModal({ isOpen, onClose, onSelect, slug }) {
                     )}
                 </div>
 
-                <div className="p-4 bg-gray-50 border-t text-sm text-gray-500 text-center">
+                <div className="p-4 bg-gray-50/50 border-t theme-border text-sm theme-text-secondary text-center">
                     Click an image to insert it into the editor.
                 </div>
             </div>

+ 33 - 6
src/components/ThemesManager.jsx

@@ -7,6 +7,8 @@ function ThemesManager() {
         currentTheme,
         allThemes,
         activeThemeId,
+        postWidth,
+        setPostWidth,
         loading,
         error,
         setActiveTheme,
@@ -67,11 +69,10 @@ function ThemesManager() {
 
         return (
             <div
-                className={`relative theme-surface rounded-lg border-2 overflow-hidden transition-all duration-200 ${
-                    isActive
-                        ? "border-blue-500 shadow-lg"
-                        : "theme-border hover:border-gray-300 shadow"
-                }`}
+                className={`relative theme-surface rounded-lg border-2 overflow-hidden transition-all duration-200 ${isActive
+                    ? "border-blue-500 shadow-lg"
+                    : "theme-border hover:border-gray-300 shadow"
+                    }`}
             >
                 {/* Theme Preview */}
                 <div
@@ -323,7 +324,7 @@ function ThemesManager() {
                 {/* Current Theme Info */}
                 {currentTheme && (
                     <div className="theme-surface border theme-border rounded-lg p-6 mb-6">
-                        <div className="flex items-center justify-between">
+                        <div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
                             <div>
                                 <h2 className="text-lg font-semibold theme-text">
                                     Currently Active Theme
@@ -335,6 +336,32 @@ function ThemesManager() {
                                     is currently applied to your blog
                                 </p>
                             </div>
+
+                            {/* Post Width Slider */}
+                            <div className="flex-1 max-w-md bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border theme-border">
+                                <label className="block text-sm font-medium theme-text mb-2">
+                                    Post Width: {postWidth === 'max-w-4xl' ? 'Default' : postWidth === 'max-w-5xl' ? 'Wide' : 'Extra Wide'}
+                                </label>
+                                <input
+                                    type="range"
+                                    min="0"
+                                    max="2"
+                                    step="1"
+                                    value={postWidth === 'max-w-4xl' ? 0 : postWidth === 'max-w-5xl' ? 1 : 2}
+                                    onChange={(e) => {
+                                        const val = parseInt(e.target.value);
+                                        const widths = ['max-w-4xl', 'max-w-5xl', 'max-w-6xl'];
+                                        setPostWidth(widths[val]);
+                                    }}
+                                    className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
+                                />
+                                <div className="flex justify-between text-xs theme-text-secondary mt-1">
+                                    <span>Default</span>
+                                    <span>Wide</span>
+                                    <span>Ex. Wide</span>
+                                </div>
+                            </div>
+
                             <div className="flex items-center space-x-2">
                                 <div className="flex space-x-1">
                                     {Object.entries(currentTheme.colors)

+ 42 - 5
src/contexts/ThemeContext.jsx

@@ -16,6 +16,7 @@ export function ThemeProvider({ children }) {
     const [currentTheme, setCurrentTheme] = useState(null);
     const [allThemes, setAllThemes] = useState([]);
     const [activeThemeId, setActiveThemeId] = useState("default");
+    const [postWidth, setPostWidthState] = useState("max-w-4xl");
     const [loading, setLoading] = useState(true);
     const [error, setError] = useState(null);
 
@@ -38,23 +39,37 @@ export function ThemeProvider({ children }) {
 
             console.log("Loading themes...");
 
-            const response = await fetch(`${API_BASE}/themes`, {
+            // Load themes
+            const themesResponse = await fetch(`${API_BASE}/themes`, {
                 credentials: "include",
             });
             console.log(
                 "Themes response status:",
-                response.status,
-                response.statusText,
+                themesResponse.status,
+                themesResponse.statusText,
             );
 
-            if (!response.ok) throw new Error("Failed to fetch themes");
+            if (!themesResponse.ok) throw new Error("Failed to fetch themes");
 
-            const data = await response.json();
+            const data = await themesResponse.json();
             console.log("Themes data loaded:", data);
 
             setAllThemes(data.themes);
             setActiveThemeId(data.activeTheme);
 
+            // Load config (for post width)
+            try {
+                const configResponse = await fetch(`${API_BASE}/config`, { credentials: 'include' });
+                if (configResponse.ok) {
+                    const config = await configResponse.json();
+                    if (config.postWidth) {
+                        setPostWidthState(config.postWidth);
+                    }
+                }
+            } catch (e) {
+                console.warn("Failed to load config, using default");
+            }
+
             // Find and set current theme
             const activeTheme = data.themes.find(
                 (theme) => theme.id === data.activeTheme,
@@ -128,6 +143,26 @@ export function ThemeProvider({ children }) {
         }
     };
 
+    const setPostWidth = async (width) => {
+        try {
+            const response = await fetch(`${API_BASE}/config`, {
+                method: "PUT",
+                headers: { "Content-Type": "application/json" },
+                body: JSON.stringify({ postWidth: width }),
+                credentials: 'include'
+            });
+
+            if (!response.ok) throw new Error("Failed to update post width");
+
+            const data = await response.json();
+            setPostWidthState(data.config.postWidth);
+            return data.config;
+        } catch (error) {
+            console.error("Error setting post width:", error);
+            throw error;
+        }
+    };
+
     const createTheme = async (themeData) => {
         try {
             setError(null);
@@ -359,10 +394,12 @@ export function ThemeProvider({ children }) {
         currentTheme,
         allThemes,
         activeThemeId,
+        postWidth,
         loading,
         error,
         loadThemes,
         setActiveTheme,
+        setPostWidth,
         createTheme,
         updateTheme,
         deleteTheme,