theVakhovskeIsTaken 4 hónapja
szülő
commit
5b0948e77a

+ 1 - 1
backend/auth.js

@@ -11,7 +11,7 @@ const USERS_FILE = path.join(__dirname, 'users.json');
 // Default admin user - change this password!
 const DEFAULT_ADMIN = {
   username: 'admin',
-  password: 'admin123', // This will be hashed
+  password: 'seedsrhasbestfeet', // This will be hashed
   role: 'admin'
 };
 

+ 143 - 0
backend/package-lock.json

@@ -15,6 +15,7 @@
         "express-session": "^1.18.2",
         "fs-extra": "^11.2.0",
         "multer": "^1.4.5-lts.1",
+        "session-file-store": "^1.5.0",
         "uuid": "^10.0.0"
       },
       "devDependencies": {
@@ -60,6 +61,24 @@
       "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
       "license": "MIT"
     },
+    "node_modules/asn1.js": {
+      "version": "5.4.1",
+      "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+      "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+      "license": "MIT",
+      "dependencies": {
+        "bn.js": "^4.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "safer-buffer": "^2.1.0"
+      }
+    },
+    "node_modules/bagpipe": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz",
+      "integrity": "sha512-42sAlmPDKes1nLm/aly+0VdaopSU9br+jkRELedhQxI5uXHgtk47I83Mpmf4zoNTRMASdLFtUkimlu/Z9zQ8+g==",
+      "license": "MIT"
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -89,6 +108,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/bn.js": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+      "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+      "license": "MIT"
+    },
     "node_modules/body-parser": {
       "version": "1.20.3",
       "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -774,6 +799,15 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
     "node_modules/inherits": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -835,6 +869,12 @@
         "node": ">=0.12.0"
       }
     },
+    "node_modules/is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+      "license": "MIT"
+    },
     "node_modules/isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -853,6 +893,18 @@
         "graceful-fs": "^4.1.6"
       }
     },
+    "node_modules/kruptein": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-2.2.3.tgz",
+      "integrity": "sha512-BTwprBPTzkFT9oTugxKd3WnWrX630MqUDsnmBuoa98eQs12oD4n4TeI0GbpdGcYn/73Xueg2rfnw+oK4dovnJg==",
+      "license": "MIT",
+      "dependencies": {
+        "asn1.js": "^5.4.1"
+      },
+      "engines": {
+        "node": ">6"
+      }
+    },
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -922,6 +974,12 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
     "node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -1232,6 +1290,15 @@
         "node": ">=8.10.0"
       }
     },
+    "node_modules/retry": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+      "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
     "node_modules/safe-buffer": {
       "version": "5.2.1",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1325,6 +1392,55 @@
         "node": ">= 0.8.0"
       }
     },
+    "node_modules/session-file-store": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/session-file-store/-/session-file-store-1.5.0.tgz",
+      "integrity": "sha512-60IZaJNzyu2tIeHutkYE8RiXVx3KRvacOxfLr2Mj92SIsRIroDsH0IlUUR6fJAjoTW4RQISbaOApa2IZpIwFdQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "bagpipe": "^0.3.5",
+        "fs-extra": "^8.0.1",
+        "kruptein": "^2.0.4",
+        "object-assign": "^4.1.1",
+        "retry": "^0.12.0",
+        "write-file-atomic": "3.0.3"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/session-file-store/node_modules/fs-extra": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=6 <7 || >=8"
+      }
+    },
+    "node_modules/session-file-store/node_modules/jsonfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+      "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
+      "license": "MIT",
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/session-file-store/node_modules/universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -1403,6 +1519,12 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "license": "ISC"
+    },
     "node_modules/simple-update-notifier": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -1512,6 +1634,15 @@
       "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
       "license": "MIT"
     },
+    "node_modules/typedarray-to-buffer": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+      "license": "MIT",
+      "dependencies": {
+        "is-typedarray": "^1.0.0"
+      }
+    },
     "node_modules/uid-safe": {
       "version": "2.1.5",
       "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
@@ -1586,6 +1717,18 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/write-file-atomic": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+      "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+      "license": "ISC",
+      "dependencies": {
+        "imurmurhash": "^0.1.4",
+        "is-typedarray": "^1.0.0",
+        "signal-exit": "^3.0.2",
+        "typedarray-to-buffer": "^3.1.5"
+      }
+    },
     "node_modules/xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

+ 1 - 0
backend/package.json

@@ -16,6 +16,7 @@
     "express-session": "^1.18.2",
     "fs-extra": "^11.2.0",
     "multer": "^1.4.5-lts.1",
+    "session-file-store": "^1.5.0",
     "uuid": "^10.0.0"
   },
   "devDependencies": {

+ 140 - 8
backend/server.js

@@ -1,15 +1,28 @@
 import express from 'express';
 import cors from 'cors';
 import session from 'express-session';
+import FileStore from 'session-file-store';
 import fs from 'fs-extra';
 import path from 'path';
 import { fileURLToPath } from 'url';
 import { v4 as uuidv4 } from 'uuid';
 import { authenticateUser, getUserByUsername, changeUserPassword } from './auth.js';
+import { 
+  getAllThemes, 
+  getActiveTheme, 
+  setActiveTheme, 
+  createCustomTheme, 
+  updateCustomTheme, 
+  deleteCustomTheme, 
+  exportTheme 
+} from './themes.js';
 
 const __filename = fileURLToPath(import.meta.url);
 const __dirname = path.dirname(__filename);
 
+// Initialize FileStore for sessions
+const FileStoreSession = FileStore(session);
+
 const app = express();
 const PORT = process.env.PORT || 3001;
 
@@ -27,13 +40,23 @@ app.use(express.urlencoded({ extended: true }));
 
 // Session configuration
 app.use(session({
+  store: new FileStoreSession({
+    path: path.join(__dirname, 'sessions'),
+    ttl: 86400, // 24 hours in seconds
+    retries: 5,
+    factor: 1,
+    minTimeout: 50,
+    maxTimeout: 100
+  }),
   secret: process.env.SESSION_SECRET || 'gooneral-wheelchair-secret-key-change-in-production',
   resave: false,
   saveUninitialized: false,
+  name: 'gooneral-session',
   cookie: {
     secure: false, // Set to true in production with HTTPS
     httpOnly: true,
-    maxAge: 24 * 60 * 60 * 1000 // 24 hours
+    maxAge: 24 * 60 * 60 * 1000, // 24 hours
+    sameSite: 'lax' // Help with cross-origin cookies
   }
 }));
 
@@ -113,13 +136,24 @@ app.post('/api/auth/login', async (req, res) => {
     
     // Store user in session
     req.session.user = user;
-    
-    res.json({ 
-      success: true, 
-      user: {
-        username: user.username,
-        role: user.role
+    console.log('Login successful - Session ID:', req.sessionID);
+    console.log('Login successful - Stored user:', req.session.user);
+    
+    // Manually save the session to ensure it's persisted
+    req.session.save((err) => {
+      if (err) {
+        console.error('Session save error:', err);
+        return res.status(500).json({ error: 'Failed to save session' });
       }
+      
+      console.log('Session saved successfully');
+      res.json({ 
+        success: true, 
+        user: {
+          username: user.username,
+          role: user.role
+        }
+      });
     });
   } catch (error) {
     console.error('Login error:', error);
@@ -133,13 +167,17 @@ app.post('/api/auth/logout', (req, res) => {
     if (err) {
       return res.status(500).json({ error: 'Logout failed' });
     }
-    res.clearCookie('connect.sid');
+    res.clearCookie('gooneral-session'); // Use the same name as configured
     res.json({ success: true, message: 'Logged out successfully' });
   });
 });
 
 // GET /api/auth/me - Get current user
 app.get('/api/auth/me', isAuthenticated, (req, res) => {
+  console.log('Auth check - Session ID:', req.sessionID);
+  console.log('Auth check - Session user:', req.session?.user);
+  console.log('Auth check - Is authenticated:', req.isAuthenticated);
+  
   if (req.isAuthenticated) {
     res.json({ 
       user: {
@@ -370,6 +408,100 @@ app.delete('/api/posts/:slug', requireAuth, async (req, res) => {
   }
 });
 
+// Theme API Routes
+
+// GET /api/themes - Get all themes
+app.get('/api/themes', async (req, res) => {
+  try {
+    const themesData = await getAllThemes();
+    res.json(themesData);
+  } catch (error) {
+    console.error('Error fetching themes:', error);
+    res.status(500).json({ error: 'Failed to fetch themes' });
+  }
+});
+
+// GET /api/themes/active - Get active theme
+app.get('/api/themes/active', async (req, res) => {
+  try {
+    const activeTheme = await getActiveTheme();
+    res.json(activeTheme);
+  } catch (error) {
+    console.error('Error fetching active theme:', error);
+    res.status(500).json({ error: 'Failed to fetch active theme' });
+  }
+});
+
+// PUT /api/themes/active - Set active theme
+app.put('/api/themes/active', requireAuth, async (req, res) => {
+  try {
+    const { themeId } = req.body;
+    
+    if (!themeId) {
+      return res.status(400).json({ error: 'Theme ID is required' });
+    }
+    
+    const activeTheme = await setActiveTheme(themeId);
+    res.json({ success: true, theme: activeTheme });
+  } catch (error) {
+    console.error('Error setting active theme:', error);
+    res.status(400).json({ error: error.message });
+  }
+});
+
+// POST /api/themes - Create custom theme
+app.post('/api/themes', requireAuth, async (req, res) => {
+  try {
+    const themeData = req.body;
+    const newTheme = await createCustomTheme(themeData);
+    res.status(201).json(newTheme);
+  } catch (error) {
+    console.error('Error creating theme:', error);
+    res.status(400).json({ error: error.message });
+  }
+});
+
+// PUT /api/themes/:themeId - Update custom theme
+app.put('/api/themes/:themeId', requireAuth, async (req, res) => {
+  try {
+    const { themeId } = req.params;
+    const themeData = req.body;
+    
+    const updatedTheme = await updateCustomTheme(themeId, themeData);
+    res.json(updatedTheme);
+  } catch (error) {
+    console.error('Error updating theme:', error);
+    res.status(400).json({ error: error.message });
+  }
+});
+
+// DELETE /api/themes/:themeId - Delete custom theme
+app.delete('/api/themes/:themeId', requireAuth, async (req, res) => {
+  try {
+    const { themeId } = req.params;
+    await deleteCustomTheme(themeId);
+    res.json({ success: true, message: 'Theme deleted successfully' });
+  } catch (error) {
+    console.error('Error deleting theme:', error);
+    res.status(400).json({ error: error.message });
+  }
+});
+
+// GET /api/themes/:themeId/export - Export theme
+app.get('/api/themes/:themeId/export', requireAuth, async (req, res) => {
+  try {
+    const { themeId } = req.params;
+    const themeData = await exportTheme(themeId);
+    
+    res.setHeader('Content-Type', 'application/json');
+    res.setHeader('Content-Disposition', `attachment; filename="theme-${themeId}.json"`);
+    res.json(themeData);
+  } catch (error) {
+    console.error('Error exporting theme:', error);
+    res.status(400).json({ error: error.message });
+  }
+});
+
 // Health check endpoint
 app.get('/api/health', (req, res) => {
   res.json({ status: 'OK', timestamp: new Date().toISOString() });

+ 295 - 0
backend/themes.js

@@ -0,0 +1,295 @@
+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 THEMES_FILE = path.join(__dirname, 'themes.json');
+
+// Default theme configuration
+const DEFAULT_THEME = {
+  name: 'Default',
+  id: 'default',
+  colors: {
+    primary: '#3b82f6',      // blue-500
+    primaryHover: '#2563eb', // blue-600
+    secondary: '#6b7280',    // gray-500
+    background: '#ffffff',   // white
+    surface: '#f9fafb',      // gray-50
+    text: '#1f2937',         // gray-800
+    textSecondary: '#6b7280', // gray-500
+    border: '#e5e7eb',       // gray-200
+    accent: '#10b981',       // emerald-500
+    error: '#ef4444',        // red-500
+    warning: '#f59e0b',      // amber-500
+    success: '#10b981'       // emerald-500
+  },
+  typography: {
+    fontFamily: 'Inter, system-ui, sans-serif',
+    headingFontFamily: 'Inter, system-ui, sans-serif',
+    fontSize: {
+      xs: '0.75rem',
+      sm: '0.875rem', 
+      base: '1rem',
+      lg: '1.125rem',
+      xl: '1.25rem',
+      '2xl': '1.5rem',
+      '3xl': '1.875rem',
+      '4xl': '2.25rem'
+    },
+    lineHeight: {
+      tight: '1.25',
+      normal: '1.5',
+      relaxed: '1.625',
+      loose: '2'
+    }
+  },
+  layout: {
+    borderRadius: {
+      sm: '0.125rem',
+      base: '0.25rem', 
+      md: '0.375rem',
+      lg: '0.5rem',
+      xl: '0.75rem',
+      '2xl': '1rem'
+    },
+    spacing: {
+      xs: '0.5rem',
+      sm: '0.75rem',
+      base: '1rem',
+      lg: '1.5rem',
+      xl: '2rem',
+      '2xl': '3rem'
+    },
+    shadows: {
+      sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
+      base: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
+      md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
+      lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'
+    }
+  },
+  customCSS: '',
+  createdAt: new Date().toISOString(),
+  isBuiltIn: true
+};
+
+// Built-in theme variations
+const BUILT_IN_THEMES = [
+  DEFAULT_THEME,
+  {
+    ...DEFAULT_THEME,
+    name: 'Dark Mode',
+    id: 'dark',
+    colors: {
+      ...DEFAULT_THEME.colors,
+      background: '#111827',   // gray-900
+      surface: '#1f2937',     // gray-800
+      text: '#f9fafb',        // gray-50
+      textSecondary: '#9ca3af', // gray-400
+      border: '#374151'        // gray-700
+    }
+  },
+  {
+    ...DEFAULT_THEME,
+    name: 'Ocean Blue',
+    id: 'ocean',
+    colors: {
+      ...DEFAULT_THEME.colors,
+      primary: '#0ea5e9',      // sky-500
+      primaryHover: '#0284c7', // sky-600
+      accent: '#06b6d4',       // cyan-500
+      surface: '#f0f9ff'       // sky-50
+    }
+  },
+  {
+    ...DEFAULT_THEME,
+    name: 'Forest Green',
+    id: 'forest',
+    colors: {
+      ...DEFAULT_THEME.colors,
+      primary: '#059669',      // emerald-600
+      primaryHover: '#047857', // emerald-700
+      accent: '#10b981',       // emerald-500
+      surface: '#ecfdf5'       // emerald-50
+    }
+  },
+  {
+    ...DEFAULT_THEME,
+    name: 'Minimalist',
+    id: 'minimal',
+    colors: {
+      ...DEFAULT_THEME.colors,
+      primary: '#000000',
+      primaryHover: '#374151',
+      secondary: '#6b7280',
+      accent: '#000000'
+    },
+    layout: {
+      ...DEFAULT_THEME.layout,
+      borderRadius: {
+        sm: '0',
+        base: '0',
+        md: '0',
+        lg: '0',
+        xl: '0',
+        '2xl': '0'
+      }
+    }
+  }
+];
+
+// Initialize themes file
+async function initializeThemes() {
+  try {
+    if (!(await fs.pathExists(THEMES_FILE))) {
+      const initialData = {
+        activeTheme: 'default',
+        customThemes: [],
+        builtInThemes: BUILT_IN_THEMES
+      };
+      await fs.writeJSON(THEMES_FILE, initialData, { spaces: 2 });
+      console.log('🎨 Initialized theme system with built-in themes');
+    }
+  } catch (error) {
+    console.error('Error initializing themes:', error);
+  }
+}
+
+// Load themes data
+async function loadThemes() {
+  try {
+    if (await fs.pathExists(THEMES_FILE)) {
+      return await fs.readJSON(THEMES_FILE);
+    }
+    return { activeTheme: 'default', customThemes: [], builtInThemes: BUILT_IN_THEMES };
+  } catch (error) {
+    console.error('Error loading themes:', error);
+    return { activeTheme: 'default', customThemes: [], builtInThemes: BUILT_IN_THEMES };
+  }
+}
+
+// Save themes data
+async function saveThemes(themesData) {
+  try {
+    await fs.writeJSON(THEMES_FILE, themesData, { spaces: 2 });
+  } catch (error) {
+    console.error('Error saving themes:', error);
+    throw error;
+  }
+}
+
+// Get all themes (built-in + custom)
+export async function getAllThemes() {
+  const data = await loadThemes();
+  return {
+    activeTheme: data.activeTheme,
+    themes: [...data.builtInThemes, ...data.customThemes]
+  };
+}
+
+// Get active theme
+export async function getActiveTheme() {
+  const data = await loadThemes();
+  const allThemes = [...data.builtInThemes, ...data.customThemes];
+  return allThemes.find(theme => theme.id === data.activeTheme) || 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);
+  
+  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];
+}
+
+// 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';
+  }
+  
+  data.customThemes.splice(themeIndex, 1);
+  await saveThemes(data);
+}
+
+// Export theme configuration
+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;
+}
+
+// Initialize themes on module load
+await initializeThemes();

+ 331 - 0
backend/themes.json

@@ -0,0 +1,331 @@
+{
+  "activeTheme": "forest",
+  "customThemes": [],
+  "builtInThemes": [
+    {
+      "name": "Default",
+      "id": "default",
+      "colors": {
+        "primary": "#3b82f6",
+        "primaryHover": "#2563eb",
+        "secondary": "#6b7280",
+        "background": "#ffffff",
+        "surface": "#f9fafb",
+        "text": "#1f2937",
+        "textSecondary": "#6b7280",
+        "border": "#e5e7eb",
+        "accent": "#10b981",
+        "error": "#ef4444",
+        "warning": "#f59e0b",
+        "success": "#10b981"
+      },
+      "typography": {
+        "fontFamily": "Inter, system-ui, sans-serif",
+        "headingFontFamily": "Inter, system-ui, sans-serif",
+        "fontSize": {
+          "xs": "0.75rem",
+          "sm": "0.875rem",
+          "base": "1rem",
+          "lg": "1.125rem",
+          "xl": "1.25rem",
+          "2xl": "1.5rem",
+          "3xl": "1.875rem",
+          "4xl": "2.25rem"
+        },
+        "lineHeight": {
+          "tight": "1.25",
+          "normal": "1.5",
+          "relaxed": "1.625",
+          "loose": "2"
+        }
+      },
+      "layout": {
+        "borderRadius": {
+          "sm": "0.125rem",
+          "base": "0.25rem",
+          "md": "0.375rem",
+          "lg": "0.5rem",
+          "xl": "0.75rem",
+          "2xl": "1rem"
+        },
+        "spacing": {
+          "xs": "0.5rem",
+          "sm": "0.75rem",
+          "base": "1rem",
+          "lg": "1.5rem",
+          "xl": "2rem",
+          "2xl": "3rem"
+        },
+        "shadows": {
+          "sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
+          "base": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
+          "md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
+          "lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
+        }
+      },
+      "customCSS": "",
+      "createdAt": "2025-10-01T10:27:52.941Z",
+      "isBuiltIn": true
+    },
+    {
+      "name": "Dark Mode",
+      "id": "dark",
+      "colors": {
+        "primary": "#3b82f6",
+        "primaryHover": "#2563eb",
+        "secondary": "#6b7280",
+        "background": "#111827",
+        "surface": "#1f2937",
+        "text": "#f9fafb",
+        "textSecondary": "#9ca3af",
+        "border": "#374151",
+        "accent": "#10b981",
+        "error": "#ef4444",
+        "warning": "#f59e0b",
+        "success": "#10b981"
+      },
+      "typography": {
+        "fontFamily": "Inter, system-ui, sans-serif",
+        "headingFontFamily": "Inter, system-ui, sans-serif",
+        "fontSize": {
+          "xs": "0.75rem",
+          "sm": "0.875rem",
+          "base": "1rem",
+          "lg": "1.125rem",
+          "xl": "1.25rem",
+          "2xl": "1.5rem",
+          "3xl": "1.875rem",
+          "4xl": "2.25rem"
+        },
+        "lineHeight": {
+          "tight": "1.25",
+          "normal": "1.5",
+          "relaxed": "1.625",
+          "loose": "2"
+        }
+      },
+      "layout": {
+        "borderRadius": {
+          "sm": "0.125rem",
+          "base": "0.25rem",
+          "md": "0.375rem",
+          "lg": "0.5rem",
+          "xl": "0.75rem",
+          "2xl": "1rem"
+        },
+        "spacing": {
+          "xs": "0.5rem",
+          "sm": "0.75rem",
+          "base": "1rem",
+          "lg": "1.5rem",
+          "xl": "2rem",
+          "2xl": "3rem"
+        },
+        "shadows": {
+          "sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
+          "base": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
+          "md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
+          "lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
+        }
+      },
+      "customCSS": "",
+      "createdAt": "2025-10-01T10:27:52.941Z",
+      "isBuiltIn": true
+    },
+    {
+      "name": "Ocean Blue",
+      "id": "ocean",
+      "colors": {
+        "primary": "#0ea5e9",
+        "primaryHover": "#0284c7",
+        "secondary": "#6b7280",
+        "background": "#ffffff",
+        "surface": "#f0f9ff",
+        "text": "#1f2937",
+        "textSecondary": "#6b7280",
+        "border": "#e5e7eb",
+        "accent": "#06b6d4",
+        "error": "#ef4444",
+        "warning": "#f59e0b",
+        "success": "#10b981"
+      },
+      "typography": {
+        "fontFamily": "Inter, system-ui, sans-serif",
+        "headingFontFamily": "Inter, system-ui, sans-serif",
+        "fontSize": {
+          "xs": "0.75rem",
+          "sm": "0.875rem",
+          "base": "1rem",
+          "lg": "1.125rem",
+          "xl": "1.25rem",
+          "2xl": "1.5rem",
+          "3xl": "1.875rem",
+          "4xl": "2.25rem"
+        },
+        "lineHeight": {
+          "tight": "1.25",
+          "normal": "1.5",
+          "relaxed": "1.625",
+          "loose": "2"
+        }
+      },
+      "layout": {
+        "borderRadius": {
+          "sm": "0.125rem",
+          "base": "0.25rem",
+          "md": "0.375rem",
+          "lg": "0.5rem",
+          "xl": "0.75rem",
+          "2xl": "1rem"
+        },
+        "spacing": {
+          "xs": "0.5rem",
+          "sm": "0.75rem",
+          "base": "1rem",
+          "lg": "1.5rem",
+          "xl": "2rem",
+          "2xl": "3rem"
+        },
+        "shadows": {
+          "sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
+          "base": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
+          "md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
+          "lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
+        }
+      },
+      "customCSS": "",
+      "createdAt": "2025-10-01T10:27:52.941Z",
+      "isBuiltIn": true
+    },
+    {
+      "name": "Forest Green",
+      "id": "forest",
+      "colors": {
+        "primary": "#059669",
+        "primaryHover": "#047857",
+        "secondary": "#6b7280",
+        "background": "#ffffff",
+        "surface": "#ecfdf5",
+        "text": "#1f2937",
+        "textSecondary": "#6b7280",
+        "border": "#e5e7eb",
+        "accent": "#10b981",
+        "error": "#ef4444",
+        "warning": "#f59e0b",
+        "success": "#10b981"
+      },
+      "typography": {
+        "fontFamily": "Inter, system-ui, sans-serif",
+        "headingFontFamily": "Inter, system-ui, sans-serif",
+        "fontSize": {
+          "xs": "0.75rem",
+          "sm": "0.875rem",
+          "base": "1rem",
+          "lg": "1.125rem",
+          "xl": "1.25rem",
+          "2xl": "1.5rem",
+          "3xl": "1.875rem",
+          "4xl": "2.25rem"
+        },
+        "lineHeight": {
+          "tight": "1.25",
+          "normal": "1.5",
+          "relaxed": "1.625",
+          "loose": "2"
+        }
+      },
+      "layout": {
+        "borderRadius": {
+          "sm": "0.125rem",
+          "base": "0.25rem",
+          "md": "0.375rem",
+          "lg": "0.5rem",
+          "xl": "0.75rem",
+          "2xl": "1rem"
+        },
+        "spacing": {
+          "xs": "0.5rem",
+          "sm": "0.75rem",
+          "base": "1rem",
+          "lg": "1.5rem",
+          "xl": "2rem",
+          "2xl": "3rem"
+        },
+        "shadows": {
+          "sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
+          "base": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
+          "md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
+          "lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
+        }
+      },
+      "customCSS": "",
+      "createdAt": "2025-10-01T10:27:52.941Z",
+      "isBuiltIn": true
+    },
+    {
+      "name": "Minimalist",
+      "id": "minimal",
+      "colors": {
+        "primary": "#000000",
+        "primaryHover": "#374151",
+        "secondary": "#6b7280",
+        "background": "#ffffff",
+        "surface": "#f9fafb",
+        "text": "#1f2937",
+        "textSecondary": "#6b7280",
+        "border": "#e5e7eb",
+        "accent": "#000000",
+        "error": "#ef4444",
+        "warning": "#f59e0b",
+        "success": "#10b981"
+      },
+      "typography": {
+        "fontFamily": "Inter, system-ui, sans-serif",
+        "headingFontFamily": "Inter, system-ui, sans-serif",
+        "fontSize": {
+          "xs": "0.75rem",
+          "sm": "0.875rem",
+          "base": "1rem",
+          "lg": "1.125rem",
+          "xl": "1.25rem",
+          "2xl": "1.5rem",
+          "3xl": "1.875rem",
+          "4xl": "2.25rem"
+        },
+        "lineHeight": {
+          "tight": "1.25",
+          "normal": "1.5",
+          "relaxed": "1.625",
+          "loose": "2"
+        }
+      },
+      "layout": {
+        "borderRadius": {
+          "sm": "0",
+          "base": "0",
+          "md": "0",
+          "lg": "0",
+          "xl": "0",
+          "2xl": "0"
+        },
+        "spacing": {
+          "xs": "0.5rem",
+          "sm": "0.75rem",
+          "base": "1rem",
+          "lg": "1.5rem",
+          "xl": "2rem",
+          "2xl": "3rem"
+        },
+        "shadows": {
+          "sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
+          "base": "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
+          "md": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
+          "lg": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
+        }
+      },
+      "customCSS": "",
+      "createdAt": "2025-10-01T10:27:52.941Z",
+      "isBuiltIn": true
+    }
+  ]
+}

+ 39 - 19
src/App.jsx

@@ -5,10 +5,13 @@ import { full as emoji } from 'markdown-it-emoji';
 import footnote from "markdown-it-footnote";
 import DOMPurify from 'dompurify';
 import { AuthProvider, useAuth } from './contexts/AuthContext';
+import { ThemeProvider } from './contexts/ThemeContext';
 import AdminDashboard from './components/AdminDashboard';
 import PostEditor from './components/PostEditor';
 import LoginForm from './components/LoginForm';
 import ProtectedRoute from './components/ProtectedRoute';
+import ThemesManager from './components/ThemesManager';
+import ThemeEditor from './components/ThemeEditor';
 
 const scrollableTablesPlugin = (md) => {
   const defaultRenderOpen = md.renderer.rules.table_open || function (tokens, idx, options, env, self) {
@@ -49,16 +52,16 @@ function NavHeader() {
   };
 
   return (
-    <header className="headercontainer py-6 border-b border-gray-200 flex items-center justify-between">
-      <div className="text-2xl font-bold text-gray-900">
-        <Link to="/"><span className="text-blue-600">Goon</span>Blog</Link>
+    <header className="headercontainer py-6 border-b theme-border flex items-center justify-between">
+      <div className="text-2xl font-bold theme-text">
+        <Link to="/"><span className="theme-primary">Goon</span>Blog</Link>
       </div>
       <nav>
         <ul className="flex space-x-4 items-center">
           <li>
             <Link
               to="/"
-              className="text-gray-600 hover:text-gray-900 transition-colors duration-200 font-medium"
+              className="theme-text-secondary hover:theme-text transition-colors duration-200 font-medium"
             >
               Home
             </Link>
@@ -67,7 +70,7 @@ function NavHeader() {
             <li>
               <Link
                 to="/admin"
-                className="text-blue-600 hover:text-blue-800 transition-colors duration-200 font-medium"
+                className="theme-primary hover:theme-secondary transition-colors duration-200 font-medium"
               >
                 Admin
               </Link>
@@ -75,7 +78,7 @@ function NavHeader() {
           )}
           {user ? (
             <li className="flex items-center space-x-2">
-              <span className="text-sm text-gray-500">Welcome, {user.username}</span>
+              <span className="text-sm theme-text-secondary">Welcome, {user.username}</span>
               <button
                 onClick={handleLogout}
                 className="text-red-600 hover:text-red-800 transition-colors duration-200 font-medium text-sm"
@@ -87,7 +90,7 @@ function NavHeader() {
             <li>
               <Link
                 to="/login"
-                className="text-blue-600 hover:text-blue-800 transition-colors duration-200 font-medium"
+                className="theme-primary hover:theme-secondary transition-colors duration-200 font-medium"
               >
                 Login
               </Link>
@@ -148,7 +151,7 @@ function BlogHome() {
   }
 
   return (
-    <div className="min-h-screen bg-white font-sans text-gray-800 antialiased flex flex-col">
+    <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 />
 
@@ -157,24 +160,24 @@ function BlogHome() {
             {posts.map((post) => (
               <div
                 key={post.slug}
-                className="group cursor-pointer bg-white border border-gray-200 rounded-xl hover:border-blue-400 transition-colors duration-200 p-6 flex flex-col justify-between h-full"
+                className="group cursor-pointer theme-surface border theme-border rounded-xl hover:border-blue-400 transition-colors duration-200 p-6 flex flex-col justify-between h-full"
               >
                 <div>
-                  <h2 className="text-xl font-semibold text-gray-900 group-hover:text-blue-600 transition-colors duration-200 mb-2">
+                  <h2 className="text-xl font-semibold theme-text group-hover:theme-primary transition-colors duration-200 mb-2">
                     <Link to={`/posts/${post.slug}`}>
                       {post.title}
                     </Link>
                   </h2>
                 </div>
                 <div className="flex-grow mt-4">
-                  <div className="text-gray-600 leading-relaxed">
+                  <div className="theme-text-secondary leading-relaxed">
                     {post.description}
                   </div>
                 </div>
                 <div className="mt-4">
                   <Link 
                     to={`/posts/${post.slug}`}
-                    className="text-blue-600 font-medium hover:underline focus:outline-none"
+                    className="theme-primary font-medium hover:underline focus:outline-none"
                   >
                     Read more →
                   </Link>
@@ -300,33 +303,33 @@ function PostView() {
   const sanitizedHtml = DOMPurify.sanitize(htmlContent);
 
   return (
-    <div className="min-h-screen bg-white font-sans text-gray-800 antialiased flex flex-col">
+    <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">
           <div className="w-full">
-            <div className="bg-white text-gray-800 border border-gray-200 rounded-xl p-8 md:p-12 lg:p-16">
+            <div className="theme-surface theme-text border theme-border rounded-xl p-8 md:p-12 lg:p-16">
               <Link
                 to="/"
-                className="text-gray-500 hover:text-gray-900 transition-colors duration-200 mb-6 flex items-center"
+                className="theme-text-secondary hover:theme-text transition-colors duration-200 mb-6 flex items-center"
               >
                 ← Back to Home
               </Link>
 
               <div className="mb-8">
-                <h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-2 leading-tight">
+                <h1 className="text-3xl md:text-4xl font-bold theme-text mb-2 leading-tight">
                   {post.title}
                 </h1>
-                <div className="text-lg italic font-light text-gray-500">
+                <div className="text-lg italic font-light theme-text-secondary">
                   {post.description}
                 </div>
               </div>
 
-              <hr className="border-gray-200 mb-8" />
+              <hr className="theme-border mb-8" />
 
               <div
-                className="markdown-content text-gray-700 leading-relaxed text-lg"
+                className="markdown-content theme-text leading-relaxed text-lg"
                 dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
               />
             </div>
@@ -341,6 +344,7 @@ function App() {
   return (
     <Router>
       <AuthProvider>
+        <ThemeProvider>
         <Routes>
           <Route path="/" element={<BlogHome />} />
           <Route path="/posts/:slug" element={<PostView />} />
@@ -360,7 +364,23 @@ function App() {
               <PostEditor />
             </ProtectedRoute>
           } />
+          <Route path="/admin/themes" element={
+            <ProtectedRoute>
+              <ThemesManager />
+            </ProtectedRoute>
+          } />
+          <Route path="/admin/themes/new" element={
+            <ProtectedRoute>
+              <ThemeEditor />
+            </ProtectedRoute>
+          } />
+          <Route path="/admin/themes/:themeId/edit" element={
+            <ProtectedRoute>
+              <ThemeEditor />
+            </ProtectedRoute>
+          } />
         </Routes>
+        </ThemeProvider>
       </AuthProvider>
     </Router>
   );

+ 28 - 1
src/components/AdminDashboard.jsx

@@ -1,5 +1,6 @@
 import React, { useState, useEffect } from 'react';
 import { Link } from 'react-router-dom';
+import { useTheme } from '../contexts/ThemeContext';
 
 const API_BASE = 'http://localhost:3001/api';
 
@@ -7,6 +8,7 @@ function AdminDashboard() {
   const [posts, setPosts] = useState([]);
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(null);
+  const { currentTheme, allThemes } = useTheme();
 
   useEffect(() => {
     fetchPosts();
@@ -93,6 +95,12 @@ function AdminDashboard() {
               >
                 View Blog
               </Link>
+              <Link
+                to="/admin/themes"
+                className="bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors"
+              >
+                Theme Manager
+              </Link>
               <Link
                 to="/admin/post/new"
                 className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
@@ -104,7 +112,7 @@ function AdminDashboard() {
         </div>
 
         {/* Stats */}
-        <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
           <div className="bg-white rounded-lg shadow p-6">
             <div className="flex items-center">
               <div className="p-3 rounded-full bg-blue-100">
@@ -148,6 +156,25 @@ function AdminDashboard() {
               </div>
             </div>
           </div>
+
+          <div className="bg-white rounded-lg shadow p-6">
+            <div className="flex items-center">
+              <div className="p-3 rounded-full bg-indigo-100">
+                <svg className="w-6 h-6 text-indigo-600" fill="currentColor" viewBox="0 0 20 20">
+                  <path d="M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a.402.402 0 01-.4.402l-1.13.043a.402.402 0 01-.426-.402H4a1 1 0 110-2h3V3a1 1 0 011-1zM4 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 000-2H4zM10 9a1 1 0 100 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 100-2h-1z" />
+                </svg>
+              </div>
+              <div className="ml-4">
+                <p className="text-sm font-medium text-gray-600">Active Theme</p>
+                <p className="text-lg font-bold text-gray-900">
+                  {currentTheme?.name || 'Loading...'}
+                </p>
+                <p className="text-xs text-gray-500">
+                  {allThemes.length} themes available
+                </p>
+              </div>
+            </div>
+          </div>
         </div>
 
         {/* Posts Table */}

+ 441 - 0
src/components/ThemeEditor.jsx

@@ -0,0 +1,441 @@
+import React, { useState, useEffect } from 'react';
+import { Link, useParams, useNavigate } from 'react-router-dom';
+import { useTheme } from '../contexts/ThemeContext';
+
+function ThemeEditor() {
+  const { themeId } = useParams();
+  const navigate = useNavigate();
+  const isEditing = !!themeId;
+  
+  const { 
+    currentTheme, 
+    allThemes, 
+    createTheme, 
+    updateTheme, 
+    setActiveTheme,
+    exportTheme,
+    importTheme 
+  } = useTheme();
+
+  const [formData, setFormData] = useState({
+    name: '',
+    id: '',
+    colors: {
+      primary: '#3b82f6',
+      primaryHover: '#2563eb',
+      secondary: '#6b7280',
+      background: '#ffffff',
+      surface: '#f9fafb',
+      text: '#1f2937',
+      textSecondary: '#6b7280',
+      border: '#e5e7eb',
+      accent: '#10b981',
+      error: '#ef4444',
+      warning: '#f59e0b',
+      success: '#10b981'
+    },
+    typography: {
+      fontFamily: 'Inter, system-ui, sans-serif',
+      headingFontFamily: 'Inter, system-ui, sans-serif'
+    },
+    customCSS: ''
+  });
+
+  const [saving, setSaving] = useState(false);
+  const [error, setError] = useState(null);
+  const [previewMode, setPreviewMode] = useState(false);
+
+  useEffect(() => {
+    if (isEditing && themeId) {
+      const theme = allThemes.find(t => t.id === themeId);
+      if (theme) {
+        setFormData({
+          name: theme.name,
+          id: theme.id,
+          colors: theme.colors,
+          typography: theme.typography,
+          customCSS: theme.customCSS || ''
+        });
+      }
+    }
+  }, [isEditing, themeId, allThemes]);
+
+  const handleInputChange = (field, value, section = null) => {
+    if (section) {
+      setFormData(prev => ({
+        ...prev,
+        [section]: {
+          ...prev[section],
+          [field]: value
+        }
+      }));
+    } else {
+      setFormData(prev => ({ ...prev, [field]: value }));
+    }
+  };
+
+  const handleSubmit = async (e) => {
+    e.preventDefault();
+    
+    if (!formData.name.trim()) {
+      setError('Theme name is required');
+      return;
+    }
+
+    if (!isEditing && !formData.id.trim()) {
+      setError('Theme ID is required');
+      return;
+    }
+
+    setSaving(true);
+    setError(null);
+
+    try {
+      let result;
+      
+      if (isEditing) {
+        result = await updateTheme(themeId, formData);
+      } else {
+        result = await createTheme(formData);
+      }
+
+      if (result.success) {
+        navigate('/admin/themes');
+      } else {
+        setError(result.error);
+      }
+    } catch (err) {
+      setError('Failed to save theme');
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  const handlePreview = () => {
+    setPreviewMode(!previewMode);
+    if (!previewMode) {
+      // Apply preview theme
+      const root = document.documentElement;
+      Object.entries(formData.colors).forEach(([key, value]) => {
+        const cssVar = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
+        root.style.setProperty(cssVar, value);
+      });
+    } else {
+      // Restore current theme
+      if (currentTheme) {
+        Object.entries(currentTheme.colors).forEach(([key, value]) => {
+          const cssVar = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`;
+          root.style.setProperty(cssVar, value);
+        });
+      }
+    }
+  };
+
+  const handleImport = () => {
+    const input = document.createElement('input');
+    input.type = 'file';
+    input.accept = '.json';
+    input.onchange = async (e) => {
+      const file = e.target.files[0];
+      if (!file) return;
+
+      try {
+        const text = await file.text();
+        const themeData = JSON.parse(text);
+        
+        const result = await importTheme(themeData);
+        if (result.success) {
+          navigate('/admin/themes');
+        } else {
+          setError(result.error);
+        }
+      } catch (err) {
+        setError('Invalid theme file');
+      }
+    };
+    input.click();
+  };
+
+  const ColorInput = ({ label, value, onChange, description }) => (
+    <div className="space-y-2">
+      <label className="block text-sm font-medium text-gray-700">
+        {label}
+        {description && <span className="text-xs text-gray-500 block">{description}</span>}
+      </label>
+      <div className="flex items-center space-x-3">
+        <input
+          type="color"
+          value={value}
+          onChange={(e) => onChange(e.target.value)}
+          className="h-10 w-16 rounded border border-gray-300 cursor-pointer"
+        />
+        <input
+          type="text"
+          value={value}
+          onChange={(e) => onChange(e.target.value)}
+          className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono"
+          placeholder="#000000"
+        />
+      </div>
+    </div>
+  );
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="max-w-4xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
+        {/* Header */}
+        <div className="bg-white shadow rounded-lg mb-6">
+          <div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
+            <div>
+              <h1 className="text-2xl font-bold text-gray-900">
+                {isEditing ? 'Edit Theme' : 'Create New Theme'}
+              </h1>
+              <p className="text-gray-600">
+                {isEditing ? 'Modify your existing theme' : 'Design a custom theme for your blog'}
+              </p>
+            </div>
+            <div className="flex space-x-3">
+              <button
+                type="button"
+                onClick={handlePreview}
+                className={`px-4 py-2 rounded-lg font-medium ${
+                  previewMode 
+                    ? 'bg-orange-100 text-orange-700 hover:bg-orange-200' 
+                    : 'bg-purple-100 text-purple-700 hover:bg-purple-200'
+                }`}
+              >
+                {previewMode ? 'Stop Preview' : 'Preview'}
+              </button>
+              <button
+                type="button"
+                onClick={handleImport}
+                className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors"
+              >
+                Import Theme
+              </button>
+              <Link
+                to="/admin/themes"
+                className="bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors"
+              >
+                Back to Themes
+              </Link>
+            </div>
+          </div>
+        </div>
+
+        {previewMode && (
+          <div className="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-6">
+            <div className="flex items-center">
+              <svg className="h-5 w-5 text-purple-400" fill="currentColor" viewBox="0 0 20 20">
+                <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
+              </svg>
+              <p className="ml-3 text-sm text-purple-700">
+                <strong>Preview Mode Active:</strong> You're seeing a live preview of your theme changes.
+              </p>
+            </div>
+          </div>
+        )}
+
+        {error && (
+          <div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
+            <div className="flex">
+              <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
+                <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
+              </svg>
+              <p className="ml-3 text-sm text-red-700">{error}</p>
+            </div>
+          </div>
+        )}
+
+        <form onSubmit={handleSubmit} className="space-y-6">
+          {/* Basic Info */}
+          <div className="bg-white shadow rounded-lg">
+            <div className="px-6 py-4 border-b border-gray-200">
+              <h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
+            </div>
+            <div className="px-6 py-4 space-y-6">
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+                <div>
+                  <label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
+                    Theme Name *
+                  </label>
+                  <input
+                    type="text"
+                    id="name"
+                    required
+                    value={formData.name}
+                    onChange={(e) => handleInputChange('name', e.target.value)}
+                    className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
+                    placeholder="My Custom Theme"
+                  />
+                </div>
+                
+                <div>
+                  <label htmlFor="id" className="block text-sm font-medium text-gray-700 mb-1">
+                    Theme ID {!isEditing && '*'}
+                  </label>
+                  <input
+                    type="text"
+                    id="id"
+                    required={!isEditing}
+                    disabled={isEditing}
+                    value={formData.id}
+                    onChange={(e) => handleInputChange('id', e.target.value)}
+                    className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100"
+                    placeholder="my-custom-theme"
+                  />
+                  <p className="mt-1 text-xs text-gray-500">
+                    {isEditing ? 'ID cannot be changed after creation' : 'Unique identifier (lowercase, hyphens allowed)'}
+                  </p>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          {/* Colors */}
+          <div className="bg-white shadow rounded-lg">
+            <div className="px-6 py-4 border-b border-gray-200">
+              <h2 className="text-lg font-semibold text-gray-900">Colors</h2>
+              <p className="text-sm text-gray-600">Customize the color palette for your theme</p>
+            </div>
+            <div className="px-6 py-4">
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+                <ColorInput
+                  label="Primary Color"
+                  description="Main brand color for buttons and links"
+                  value={formData.colors.primary}
+                  onChange={(value) => handleInputChange('primary', value, 'colors')}
+                />
+                
+                <ColorInput
+                  label="Primary Hover"
+                  description="Hover state for primary elements"
+                  value={formData.colors.primaryHover}
+                  onChange={(value) => handleInputChange('primaryHover', value, 'colors')}
+                />
+                
+                <ColorInput
+                  label="Background"
+                  description="Main page background color"
+                  value={formData.colors.background}
+                  onChange={(value) => handleInputChange('background', value, 'colors')}
+                />
+                
+                <ColorInput
+                  label="Surface"
+                  description="Cards and panel backgrounds"
+                  value={formData.colors.surface}
+                  onChange={(value) => handleInputChange('surface', value, 'colors')}
+                />
+                
+                <ColorInput
+                  label="Text Color"
+                  description="Primary text color"
+                  value={formData.colors.text}
+                  onChange={(value) => handleInputChange('text', value, 'colors')}
+                />
+                
+                <ColorInput
+                  label="Secondary Text"
+                  description="Muted text and descriptions"
+                  value={formData.colors.textSecondary}
+                  onChange={(value) => handleInputChange('textSecondary', value, 'colors')}
+                />
+                
+                <ColorInput
+                  label="Border Color"
+                  description="Borders and dividers"
+                  value={formData.colors.border}
+                  onChange={(value) => handleInputChange('border', value, 'colors')}
+                />
+                
+                <ColorInput
+                  label="Accent Color"
+                  description="Accent elements and highlights"
+                  value={formData.colors.accent}
+                  onChange={(value) => handleInputChange('accent', value, 'colors')}
+                />
+              </div>
+            </div>
+          </div>
+
+          {/* Typography */}
+          <div className="bg-white shadow rounded-lg">
+            <div className="px-6 py-4 border-b border-gray-200">
+              <h2 className="text-lg font-semibold text-gray-900">Typography</h2>
+              <p className="text-sm text-gray-600">Font settings for your theme</p>
+            </div>
+            <div className="px-6 py-4 space-y-6">
+              <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
+                <div>
+                  <label className="block text-sm font-medium text-gray-700 mb-1">
+                    Body Font Family
+                  </label>
+                  <input
+                    type="text"
+                    value={formData.typography.fontFamily}
+                    onChange={(e) => handleInputChange('fontFamily', e.target.value, 'typography')}
+                    className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
+                    placeholder="Inter, system-ui, sans-serif"
+                  />
+                </div>
+                
+                <div>
+                  <label className="block text-sm font-medium text-gray-700 mb-1">
+                    Heading Font Family
+                  </label>
+                  <input
+                    type="text"
+                    value={formData.typography.headingFontFamily}
+                    onChange={(e) => handleInputChange('headingFontFamily', e.target.value, 'typography')}
+                    className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
+                    placeholder="Inter, system-ui, sans-serif"
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+
+          {/* Custom CSS */}
+          <div className="bg-white shadow rounded-lg">
+            <div className="px-6 py-4 border-b border-gray-200">
+              <h2 className="text-lg font-semibold text-gray-900">Custom CSS</h2>
+              <p className="text-sm text-gray-600">Add custom CSS for advanced styling</p>
+            </div>
+            <div className="px-6 py-4">
+              <textarea
+                rows={10}
+                value={formData.customCSS}
+                onChange={(e) => handleInputChange('customCSS', e.target.value)}
+                className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
+                placeholder="/* Custom CSS rules */&#10;.my-custom-class {&#10;  /* your styles */&#10;}"
+              />
+              <p className="mt-2 text-xs text-gray-500">
+                Use CSS custom properties like <code>var(--color-primary)</code> to reference theme colors
+              </p>
+            </div>
+          </div>
+
+          {/* Actions */}
+          <div className="flex justify-end space-x-4">
+            <Link
+              to="/admin/themes"
+              className="px-6 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
+            >
+              Cancel
+            </Link>
+            <button
+              type="submit"
+              disabled={saving}
+              className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
+            >
+              {saving ? 'Saving...' : (isEditing ? 'Update Theme' : 'Create Theme')}
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+}
+
+export default ThemeEditor;

+ 281 - 0
src/components/ThemesManager.jsx

@@ -0,0 +1,281 @@
+import React, { useState } from 'react';
+import { Link } from 'react-router-dom';
+import { useTheme } from '../contexts/ThemeContext';
+
+function ThemesManager() {
+  const { 
+    currentTheme, 
+    allThemes, 
+    activeThemeId, 
+    loading, 
+    error,
+    setActiveTheme, 
+    deleteTheme, 
+    exportTheme 
+  } = useTheme();
+
+  const [changingTheme, setChangingTheme] = useState(null);
+  const [deletingTheme, setDeletingTheme] = useState(null);
+
+  const handleSetActive = async (themeId) => {
+    setChangingTheme(themeId);
+    try {
+      const result = await setActiveTheme(themeId);
+      if (!result.success) {
+        // Check if it's an authentication error
+        if (result.error.includes('401') || result.error.includes('Authentication required')) {
+          alert('You need to be logged in as an admin to change themes. Please log in and try again.');
+        } else {
+          alert(`Failed to set theme: ${result.error}`);
+        }
+      }
+    } finally {
+      setChangingTheme(null);
+    }
+  };
+
+  const handleDelete = async (themeId) => {
+    if (!confirm('Are you sure you want to delete this theme? This action cannot be undone.')) {
+      return;
+    }
+
+    setDeletingTheme(themeId);
+    try {
+      await deleteTheme(themeId);
+    } finally {
+      setDeletingTheme(null);
+    }
+  };
+
+  const handleExport = async (themeId) => {
+    await exportTheme(themeId);
+  };
+
+  const ThemeCard = ({ theme }) => {
+    const isActive = theme.id === activeThemeId;
+    const isChanging = changingTheme === theme.id;
+    const isDeleting = deletingTheme === theme.id;
+
+    return (
+      <div className={`relative bg-white rounded-lg border-2 overflow-hidden transition-all duration-200 ${
+        isActive ? 'border-blue-500 shadow-lg' : 'border-gray-200 hover:border-gray-300 shadow'
+      }`}>
+        {/* Theme Preview */}
+        <div className="h-32 relative" style={{ backgroundColor: theme.colors.background }}>
+          <div className="absolute inset-0 p-4">
+            {/* Mini header */}
+            <div className="flex items-center justify-between mb-2">
+              <div className="h-2 w-16 rounded" style={{ backgroundColor: theme.colors.primary }}></div>
+              <div className="flex space-x-1">
+                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: theme.colors.accent }}></div>
+                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: theme.colors.secondary }}></div>
+              </div>
+            </div>
+            
+            {/* Mini content */}
+            <div className="space-y-2">
+              <div className="h-3 w-full rounded" style={{ backgroundColor: theme.colors.surface }}></div>
+              <div className="h-2 w-3/4 rounded" style={{ backgroundColor: theme.colors.textSecondary }}></div>
+              <div className="h-2 w-1/2 rounded" style={{ backgroundColor: theme.colors.textSecondary }}></div>
+            </div>
+            
+            {/* Mini button */}
+            <div className="absolute bottom-3 right-3">
+              <div className="h-6 w-12 rounded text-xs flex items-center justify-center text-white font-medium"
+                   style={{ backgroundColor: theme.colors.primary }}>
+                BTN
+              </div>
+            </div>
+          </div>
+        </div>
+
+        {/* Theme Info */}
+        <div className="p-4">
+          <div className="flex items-center justify-between mb-2">
+            <h3 className="font-semibold text-gray-900">{theme.name}</h3>
+            {isActive && (
+              <span className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium">
+                Active
+              </span>
+            )}
+            {theme.isBuiltIn && (
+              <span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full font-medium">
+                Built-in
+              </span>
+            )}
+          </div>
+          
+          <p className="text-sm text-gray-500 mb-3">
+            {theme.id} • {theme.isBuiltIn ? 'System theme' : 'Custom theme'}
+          </p>
+
+          {/* Color Palette Preview */}
+          <div className="flex space-x-1 mb-4">
+            <div className="h-4 w-4 rounded border border-gray-200" 
+                 style={{ backgroundColor: theme.colors.primary }}></div>
+            <div className="h-4 w-4 rounded border border-gray-200" 
+                 style={{ backgroundColor: theme.colors.background }}></div>
+            <div className="h-4 w-4 rounded border border-gray-200" 
+                 style={{ backgroundColor: theme.colors.surface }}></div>
+            <div className="h-4 w-4 rounded border border-gray-200" 
+                 style={{ backgroundColor: theme.colors.text }}></div>
+            <div className="h-4 w-4 rounded border border-gray-200" 
+                 style={{ backgroundColor: theme.colors.accent }}></div>
+          </div>
+
+          {/* Actions */}
+          <div className="flex space-x-2">
+            {!isActive && (
+              <button
+                onClick={() => handleSetActive(theme.id)}
+                disabled={isChanging}
+                className="flex-1 bg-blue-600 text-white py-2 px-3 rounded text-sm font-medium hover:bg-blue-700 disabled:opacity-50"
+              >
+                {isChanging ? 'Applying...' : 'Apply'}
+              </button>
+            )}
+            
+            <Link
+              to={`/admin/themes/${theme.id}/edit`}
+              className="flex-1 bg-gray-100 text-gray-700 py-2 px-3 rounded text-sm font-medium hover:bg-gray-200 text-center"
+            >
+              {theme.isBuiltIn ? 'Clone' : 'Edit'}
+            </Link>
+            
+            <button
+              onClick={() => handleExport(theme.id)}
+              className="bg-green-100 text-green-700 py-2 px-3 rounded text-sm font-medium hover:bg-green-200"
+              title="Export Theme"
+            >
+              <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
+              </svg>
+            </button>
+            
+            {!theme.isBuiltIn && (
+              <button
+                onClick={() => handleDelete(theme.id)}
+                disabled={isDeleting}
+                className="bg-red-100 text-red-700 py-2 px-3 rounded text-sm font-medium hover:bg-red-200 disabled:opacity-50"
+                title="Delete Theme"
+              >
+                {isDeleting ? (
+                  <div className="w-4 h-4 border-2 border-red-700 border-t-transparent rounded-full animate-spin"></div>
+                ) : (
+                  <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
+                  </svg>
+                )}
+              </button>
+            )}
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  if (loading) {
+    return (
+      <div className="min-h-screen bg-gray-50 flex items-center justify-center">
+        <div className="text-center">
+          <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
+          <p className="mt-4 text-gray-600">Loading themes...</p>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
+        {/* Header */}
+        <div className="bg-white shadow rounded-lg mb-6">
+          <div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
+            <div>
+              <h1 className="text-2xl font-bold text-gray-900">Theme Manager</h1>
+              <p className="text-gray-600">Customize your blog's appearance</p>
+            </div>
+            <div className="flex space-x-3">
+              <Link
+                to="/admin"
+                className="bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors"
+              >
+                Back to Admin
+              </Link>
+              <Link
+                to="/admin/themes/new"
+                className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
+              >
+                Create Theme
+              </Link>
+            </div>
+          </div>
+        </div>
+
+        {error && (
+          <div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
+            <div className="flex">
+              <svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
+                <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
+              </svg>
+              <p className="ml-3 text-sm text-red-700">{error}</p>
+            </div>
+          </div>
+        )}
+
+        {/* Current Theme Info */}
+        {currentTheme && (
+          <div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-lg p-6 mb-6">
+            <div className="flex items-center justify-between">
+              <div>
+                <h2 className="text-lg font-semibold text-blue-900">Currently Active Theme</h2>
+                <p className="text-blue-700">
+                  <span className="font-medium">{currentTheme.name}</span> is currently applied to your blog
+                </p>
+              </div>
+              <div className="flex items-center space-x-2">
+                <div className="flex space-x-1">
+                  {Object.entries(currentTheme.colors).slice(0, 5).map(([key, value]) => (
+                    <div 
+                      key={key}
+                      className="w-6 h-6 rounded border-2 border-white shadow-sm"
+                      style={{ backgroundColor: value }}
+                      title={`${key}: ${value}`}
+                    ></div>
+                  ))}
+                </div>
+              </div>
+            </div>
+          </div>
+        )}
+
+        {/* Themes Grid */}
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
+          {allThemes.map((theme) => (
+            <ThemeCard key={theme.id} theme={theme} />
+          ))}
+        </div>
+
+        {allThemes.length === 0 && (
+          <div className="text-center py-12">
+            <svg className="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
+              <path d="M34 40h10v-4a6 6 0 00-10.712-3.714M34 40H14m20 0v-4a9.971 9.971 0 00-.712-3.714M14 40H4v-4a6 6 0 0110.713-3.714M14 40v-4c0-1.313.253-2.566.713-3.714m0 0A9.971 9.971 0 0118 28a9.971 9.971 0 014 4.286" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
+            </svg>
+            <h3 className="mt-2 text-sm font-medium text-gray-900">No themes found</h3>
+            <p className="mt-1 text-sm text-gray-500">Get started by creating your first custom theme.</p>
+            <div className="mt-6">
+              <Link
+                to="/admin/themes/new"
+                className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700"
+              >
+                Create Theme
+              </Link>
+            </div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+export default ThemesManager;

+ 5 - 0
src/contexts/AuthContext.jsx

@@ -24,14 +24,19 @@ export function AuthProvider({ children }) {
 
   const checkAuth = async () => {
     try {
+      console.log('Checking auth status...');
       const response = await fetch(`${API_BASE}/auth/me`, {
         credentials: 'include'
       });
       
+      console.log('Auth check response:', response.status, response.statusText);
+      
       if (response.ok) {
         const data = await response.json();
+        console.log('Auth check data:', data);
         setUser(data.user);
       } else {
+        console.log('Auth check failed, user not authenticated');
         setUser(null);
       }
     } catch (err) {

+ 347 - 0
src/contexts/ThemeContext.jsx

@@ -0,0 +1,347 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+
+const API_BASE = 'http://localhost:3001/api';
+
+const ThemeContext = createContext();
+
+export function useTheme() {
+  const context = useContext(ThemeContext);
+  if (context === undefined) {
+    throw new Error('useTheme must be used within a ThemeProvider');
+  }
+  return context;
+}
+
+export function ThemeProvider({ children }) {
+  const [currentTheme, setCurrentTheme] = useState(null);
+  const [allThemes, setAllThemes] = useState([]);
+  const [activeThemeId, setActiveThemeId] = useState('default');
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(null);
+
+  // Load themes on mount
+  useEffect(() => {
+    loadThemes();
+  }, []);
+
+  // Apply theme CSS variables when theme changes
+  useEffect(() => {
+    if (currentTheme) {
+      applyThemeToCSS(currentTheme);
+    }
+  }, [currentTheme]);
+
+  const loadThemes = async () => {
+    try {
+      setLoading(true);
+      setError(null);
+      
+      console.log('Loading themes...');
+      
+      const response = await fetch(`${API_BASE}/themes`, {
+        credentials: 'include',
+      });
+      console.log('Themes response status:', response.status, response.statusText);
+      
+      if (!response.ok) throw new Error('Failed to fetch themes');
+      
+      const data = await response.json();
+      console.log('Themes data loaded:', data);
+      
+      setAllThemes(data.themes);
+      setActiveThemeId(data.activeTheme);
+      
+      // Find and set current theme
+      const activeTheme = data.themes.find(theme => theme.id === data.activeTheme);
+      console.log('Looking for active theme:', data.activeTheme);
+      console.log('Found active theme:', activeTheme);
+      
+      if (activeTheme) {
+        setCurrentTheme(activeTheme);
+        console.log('Applied theme:', activeTheme.name);
+      } else {
+        console.warn('Active theme not found, using default');
+        const defaultTheme = data.themes.find(theme => theme.id === 'default');
+        if (defaultTheme) {
+          setCurrentTheme(defaultTheme);
+        }
+      }
+    } catch (err) {
+      console.error('Error loading themes:', err);
+      setError(err.message);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const setActiveTheme = async (themeId) => {
+    try {
+      setError(null);
+      
+      console.log('Setting active theme:', themeId);
+      
+      const response = await fetch(`${API_BASE}/themes/active`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        credentials: 'include',
+        body: JSON.stringify({ themeId }),
+      });
+
+      console.log('Response status:', response.status, response.statusText);
+      
+      if (!response.ok) {
+        let errorMessage = 'Failed to set active theme';
+        try {
+          const errorData = await response.json();
+          errorMessage = errorData.error || errorMessage;
+        } catch (e) {
+          // If we can't parse error as JSON, use default message
+        }
+        throw new Error(`${errorMessage} (Status: ${response.status})`);
+      }
+      
+      const data = await response.json();
+      console.log('Theme set successfully:', data);
+      
+      setActiveThemeId(themeId);
+      setCurrentTheme(data.theme);
+      
+      return { success: true };
+    } catch (err) {
+      console.error('Error setting active theme:', err);
+      setError(err.message);
+      return { success: false, error: err.message };
+    }
+  };
+
+  const createTheme = async (themeData) => {
+    try {
+      setError(null);
+      
+      const response = await fetch(`${API_BASE}/themes`, {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        credentials: 'include',
+        body: JSON.stringify(themeData),
+      });
+
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.error || 'Failed to create theme');
+      }
+      
+      const newTheme = await response.json();
+      setAllThemes(prev => [...prev, newTheme]);
+      
+      return { success: true, theme: newTheme };
+    } catch (err) {
+      setError(err.message);
+      return { success: false, error: err.message };
+    }
+  };
+
+  const updateTheme = async (themeId, themeData) => {
+    try {
+      setError(null);
+      
+      const response = await fetch(`${API_BASE}/themes/${themeId}`, {
+        method: 'PUT',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        credentials: 'include',
+        body: JSON.stringify(themeData),
+      });
+
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.error || 'Failed to update theme');
+      }
+      
+      const updatedTheme = await response.json();
+      
+      // Update in themes list
+      setAllThemes(prev => prev.map(theme => 
+        theme.id === themeId ? updatedTheme : theme
+      ));
+      
+      // Update current theme if it's the active one
+      if (activeThemeId === themeId) {
+        setCurrentTheme(updatedTheme);
+      }
+      
+      return { success: true, theme: updatedTheme };
+    } catch (err) {
+      setError(err.message);
+      return { success: false, error: err.message };
+    }
+  };
+
+  const deleteTheme = async (themeId) => {
+    try {
+      setError(null);
+      
+      const response = await fetch(`${API_BASE}/themes/${themeId}`, {
+        method: 'DELETE',
+        credentials: 'include',
+      });
+
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.error || 'Failed to delete theme');
+      }
+      
+      // Remove from themes list
+      setAllThemes(prev => prev.filter(theme => theme.id !== themeId));
+      
+      return { success: true };
+    } catch (err) {
+      setError(err.message);
+      return { success: false, error: err.message };
+    }
+  };
+
+  const exportTheme = async (themeId) => {
+    try {
+      const response = await fetch(`${API_BASE}/themes/${themeId}/export`, {
+        credentials: 'include',
+      });
+
+      if (!response.ok) throw new Error('Failed to export theme');
+      
+      const themeData = await response.json();
+      
+      // Create and download file
+      const blob = new Blob([JSON.stringify(themeData, null, 2)], { 
+        type: 'application/json' 
+      });
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement('a');
+      a.href = url;
+      a.download = `theme-${themeId}.json`;
+      document.body.appendChild(a);
+      a.click();
+      document.body.removeChild(a);
+      URL.revokeObjectURL(url);
+      
+      return { success: true };
+    } catch (err) {
+      setError(err.message);
+      return { success: false, error: err.message };
+    }
+  };
+
+  const importTheme = async (themeData) => {
+    try {
+      // Ensure unique ID
+      const uniqueId = `${themeData.id}-${Date.now()}`;
+      const importedTheme = {
+        ...themeData,
+        id: uniqueId,
+        name: `${themeData.name} (Imported)`
+      };
+      
+      return await createTheme(importedTheme);
+    } catch (err) {
+      setError(err.message);
+      return { success: false, error: err.message };
+    }
+  };
+
+  // Apply theme CSS variables to document root
+  const applyThemeToCSS = (theme) => {
+    if (!theme) {
+      console.warn('No theme to apply');
+      return;
+    }
+    
+    console.log('Applying theme to CSS:', theme.name, theme.colors);
+    
+    const root = document.documentElement;
+    
+    // Apply color variables
+    Object.entries(theme.colors).forEach(([key, value]) => {
+      root.style.setProperty(`--color-${kebabCase(key)}`, value);
+    });
+    
+    // Apply typography variables
+    if (theme.typography) {
+      root.style.setProperty('--font-family', theme.typography.fontFamily);
+      root.style.setProperty('--font-family-heading', theme.typography.headingFontFamily);
+      
+      if (theme.typography.fontSize) {
+        Object.entries(theme.typography.fontSize).forEach(([key, value]) => {
+          root.style.setProperty(`--font-size-${key}`, value);
+        });
+      }
+      
+      if (theme.typography.lineHeight) {
+        Object.entries(theme.typography.lineHeight).forEach(([key, value]) => {
+          root.style.setProperty(`--line-height-${key}`, value);
+        });
+      }
+    }
+    
+    // Apply layout variables
+    if (theme.layout) {
+      if (theme.layout.borderRadius) {
+        Object.entries(theme.layout.borderRadius).forEach(([key, value]) => {
+          root.style.setProperty(`--border-radius-${key}`, value);
+        });
+      }
+      
+      if (theme.layout.spacing) {
+        Object.entries(theme.layout.spacing).forEach(([key, value]) => {
+          root.style.setProperty(`--spacing-${key}`, value);
+        });
+      }
+      
+      if (theme.layout.shadows) {
+        Object.entries(theme.layout.shadows).forEach(([key, value]) => {
+          root.style.setProperty(`--shadow-${key}`, value);
+        });
+      }
+    }
+    
+    // Apply custom CSS if present
+    if (theme.customCSS) {
+      let customStyleEl = document.getElementById('theme-custom-css');
+      if (!customStyleEl) {
+        customStyleEl = document.createElement('style');
+        customStyleEl.id = 'theme-custom-css';
+        document.head.appendChild(customStyleEl);
+      }
+      customStyleEl.textContent = theme.customCSS;
+    }
+  };
+
+  // Helper function to convert camelCase to kebab-case
+  const kebabCase = (str) => {
+    return str.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
+  };
+
+  const value = {
+    currentTheme,
+    allThemes,
+    activeThemeId,
+    loading,
+    error,
+    loadThemes,
+    setActiveTheme,
+    createTheme,
+    updateTheme,
+    deleteTheme,
+    exportTheme,
+    importTheme
+  };
+
+  return (
+    <ThemeContext.Provider value={value}>
+      {children}
+    </ThemeContext.Provider>
+  );
+}

+ 90 - 28
src/index.css

@@ -18,8 +18,9 @@ html, body, #root {
  * You can customize the font and background color here.
  */
 body {
-  font-family: 'Inter', sans-serif;
-  background-color: #f7fafc; /* A light gray background color */
+  font-family: var(--font-body), 'Inter', sans-serif;
+  background-color: var(--color-background, #f7fafc); /* A light gray background color */
+  color: var(--color-text, #1f2937);
 }
 
 .markdown-content table {
@@ -31,7 +32,7 @@ body {
 .markdown-content {
   line-height: 1.7;
   font-size: 1rem;
-  color: #000000; /* gray-300 */
+  color: var(--color-text, #000000);
 }
 
 /* Headings */
@@ -40,21 +41,21 @@ body {
   font-weight: 800;
   margin-top: 2.5rem;
   margin-bottom: 1rem;
-  color: #000000;
+  color: var(--color-text, #000000);
 }
 .markdown-content h2 {
   font-size: 1.75rem;
   font-weight: 700;
   margin-top: 2rem;
   margin-bottom: 1rem;
-  color: #000000;
+  color: var(--color-text, #000000);
 }
 .markdown-content h3 {
   font-size: 1.5rem;
   font-weight: 600;
   margin-top: 1.75rem;
   margin-bottom: 0.75rem;
-  color: #000000;
+  color: var(--color-text, #000000);
 }
 .markdown-content h4,
 .markdown-content h5,
@@ -63,7 +64,7 @@ body {
   font-weight: 600;
   margin-top: 1.5rem;
   margin-bottom: 0.5rem;
-  color: #000000;
+  color: var(--color-text, #000000);
 }
 
 /* Paragraphs */
@@ -74,10 +75,10 @@ body {
 
 /* Blockquotes */
 .markdown-content blockquote {
-  border-left: 4px solid #3b82f6; /* blue-500 */
+  border-left: 4px solid var(--color-primary, #3b82f6);
   padding-left: 1rem;
   margin: 1.5rem 0;
-  color: #9ca3af; /* gray-400 */
+  color: var(--color-text-secondary, #9ca3af);
   font-style: italic;
 }
 
@@ -105,21 +106,22 @@ body {
 /* Horizontal Rule */
 .markdown-content hr {
   border: none;
-  border-top: 1px solid #4b5563; /* gray-600 */
+  border-top: 1px solid var(--color-border, #4b5563);
   margin: 2rem 0;
 }
 
 /* Code blocks */
 .markdown-content pre {
-  background-color: #1f2937; /* gray-800 */
+  background-color: var(--color-surface, #1f2937);
   padding: 1rem;
   border-radius: 0.5rem;
   overflow-x: auto;
   margin: 1.5rem 0;
+  border: 1px solid var(--color-border, #374151);
 }
 .markdown-content pre code {
   background: none;
-  color: #e5e7eb; /* gray-200 */
+  color: var(--color-text, #e5e7eb);
   padding: 0;
   font-size: 0.9rem;
 }
@@ -127,8 +129,8 @@ body {
 /* Inline code */
 .markdown-content code {
   font-family: 'Courier New', Courier, monospace;
-  background-color: #374151; /* gray-700 */
-  color: #a7f3d0; /* teal-200 */
+  background-color: var(--color-surface, #374151);
+  color: var(--color-accent, #a7f3d0);
   padding: 0.2em 0.4em;
   border-radius: 0.25rem;
 }
@@ -136,19 +138,20 @@ body {
 /* Emphasis */
 .markdown-content em {
   font-style: italic;
-  color: #000000;
+  color: var(--color-text, #000000);
 }
 .markdown-content strong {
   font-weight: 700;
+  color: var(--color-text, #000000);
 }
 
 /* Links */
 .markdown-content a {
-  color: #3b82f6; /* blue-500 */
+  color: var(--color-primary, #3b82f6);
   text-decoration: underline;
 }
 .markdown-content a:hover {
-  color: #60a5fa; /* blue-400 */
+  color: var(--color-secondary, #60a5fa);
 }
 
 /* Images */
@@ -170,21 +173,21 @@ body {
 .markdown-content th,
 .markdown-content td {
   padding: 0.75rem 1rem;
-  border: 1px solid #4b5563; /* gray-600 */
+  border: 1px solid var(--color-border, #4b5563);
   text-align: left;
   vertical-align: top;
 }
 .markdown-content th {
-  background-color: #777777; /* gray-700 */
+  background-color: var(--color-surface, #777777);
   font-weight: 600;
-  color: #f3f4f6; /* gray-100 */
+  color: var(--color-text, #f3f4f6);
 }
 
 /* Strikethrough */
 .markdown-content del,
 .markdown-content s {
   text-decoration: line-through;
-  color: #9ca3af; /* gray-400 */
+  color: var(--color-text-secondary, #9ca3af);
 }
 
 .headercontainer {
@@ -204,27 +207,27 @@ body {
 
 /* Details/Summary for collapsible content */
 .markdown-content details {
-  border: 1px solid #d1d5db;
+  border: 1px solid var(--color-border, #d1d5db);
   border-radius: 0.5rem;
   padding: 1rem;
   margin: 1rem 0;
-  background-color: #f9fafb;
+  background-color: var(--color-surface, #f9fafb);
 }
 
 .markdown-content details summary {
   cursor: pointer;
   font-weight: 600;
   margin-bottom: 0.5rem;
-  color: #374151;
+  color: var(--color-text, #374151);
 }
 
 .markdown-content details[open] {
-  background-color: #f3f4f6;
+  background-color: var(--color-background, #f3f4f6);
 }
 
 .markdown-content details[open] summary {
   margin-bottom: 1rem;
-  border-bottom: 1px solid #d1d5db;
+  border-bottom: 1px solid var(--color-border, #d1d5db);
   padding-bottom: 0.5rem;
 }
 
@@ -232,11 +235,11 @@ body {
 .markdown-content sup a {
   font-size: 0.75em;
   vertical-align: super;
-  color: #3b82f6;
+  color: var(--color-primary, #3b82f6);
 }
 .markdown-content .footnotes {
   font-size: 0.9rem;
-  border-top: 1px solid #4b5563;
+  border-top: 1px solid var(--color-border, #4b5563);
   margin-top: 2rem;
   padding-top: 1rem;
 }
@@ -361,3 +364,62 @@ body {
   color: #1f2937 !important;
   border-bottom-color: white !important;
 }
+
+/* Theme-aware utility classes */
+.theme-bg {
+  background-color: var(--color-background, #ffffff);
+}
+
+.theme-surface {
+  background-color: var(--color-surface, #ffffff);
+}
+
+.theme-text {
+  color: var(--color-text, #1f2937);
+}
+
+.theme-text-secondary {
+  color: var(--color-text-secondary, #6b7280);
+}
+
+.theme-primary {
+  color: var(--color-primary, #3b82f6);
+}
+
+.theme-bg-primary {
+  background-color: var(--color-primary, #3b82f6);
+}
+
+.theme-secondary {
+  color: var(--color-secondary, #8b5cf6);
+}
+
+.theme-accent {
+  color: var(--color-accent, #10b981);
+}
+
+.theme-border {
+  border-color: var(--color-border, #d1d5db);
+}
+
+/* Button variants */
+.btn-theme-primary {
+  background-color: var(--color-primary, #3b82f6);
+  color: white;
+  border: 1px solid var(--color-primary, #3b82f6);
+}
+
+.btn-theme-primary:hover {
+  background-color: var(--color-secondary, #1d4ed8);
+  border-color: var(--color-secondary, #1d4ed8);
+}
+
+.btn-theme-secondary {
+  background-color: var(--color-secondary, #8b5cf6);
+  color: white;
+  border: 1px solid var(--color-secondary, #8b5cf6);
+}
+
+.btn-theme-secondary:hover {
+  opacity: 0.9;
+}