|
|
@@ -1,441 +1,570 @@
|
|
|
-import React, { useState, useEffect } from 'react';
|
|
|
-import { Link, useParams, useNavigate } from 'react-router-dom';
|
|
|
-import { useTheme } from '../contexts/ThemeContext';
|
|
|
+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
|
|
|
+ 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 || "",
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
- }));
|
|
|
- } 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');
|
|
|
+ }, [isEditing, themeId, allThemes]);
|
|
|
+
|
|
|
+ const handleInputChange = (field, value, section = null) => {
|
|
|
+ if (section) {
|
|
|
+ setFormData((prev) => ({
|
|
|
+ ...prev,
|
|
|
+ [section]: {
|
|
|
+ ...prev[section],
|
|
|
+ [field]: value,
|
|
|
+ },
|
|
|
+ }));
|
|
|
} else {
|
|
|
- setError(result.error);
|
|
|
+ setFormData((prev) => ({ ...prev, [field]: value }));
|
|
|
}
|
|
|
- } 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>
|
|
|
+ const handleSubmit = async (e) => {
|
|
|
+ e.preventDefault();
|
|
|
|
|
|
- {/* 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')}
|
|
|
+ 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 theme-text">
|
|
|
+ {label}
|
|
|
+ {description && (
|
|
|
+ <span className="text-xs theme-text-secondary 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 theme-border cursor-pointer"
|
|
|
/>
|
|
|
-
|
|
|
- <ColorInput
|
|
|
- label="Accent Color"
|
|
|
- description="Accent elements and highlights"
|
|
|
- value={formData.colors.accent}
|
|
|
- onChange={(value) => handleInputChange('accent', value, 'colors')}
|
|
|
+ <input
|
|
|
+ type="text"
|
|
|
+ value={value}
|
|
|
+ onChange={(e) => onChange(e.target.value)}
|
|
|
+ className="flex-1 px-3 py-2 border theme-border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm font-mono"
|
|
|
+ placeholder="#000000"
|
|
|
/>
|
|
|
- </div>
|
|
|
</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"
|
|
|
- />
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen theme-bg">
|
|
|
+ <div className="max-w-4xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
|
|
+ {/* Header */}
|
|
|
+ <div className="theme-surface shadow rounded-lg mb-6">
|
|
|
+ <div className="px-6 py-4 border-b theme-border flex justify-between items-center">
|
|
|
+ <div>
|
|
|
+ <h1 className="text-2xl font-bold theme-text">
|
|
|
+ {isEditing ? "Edit Theme" : "Create New Theme"}
|
|
|
+ </h1>
|
|
|
+ <p className="theme-text-secondary">
|
|
|
+ {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
|
|
|
+ ? "btn-theme-secondary"
|
|
|
+ : "btn-theme-primary"
|
|
|
+ }`}
|
|
|
+ >
|
|
|
+ {previewMode ? "Stop Preview" : "Preview"}
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={handleImport}
|
|
|
+ className="btn-theme-primary text-white px-4 py-2 rounded-lg transition-colors"
|
|
|
+ >
|
|
|
+ Import Theme
|
|
|
+ </button>
|
|
|
+ <Link
|
|
|
+ to="/admin/themes"
|
|
|
+ className="btn-theme-secondary text-white px-4 py-2 rounded-lg transition-colors"
|
|
|
+ >
|
|
|
+ Back to Themes
|
|
|
+ </Link>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</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 */ .my-custom-class { /* your styles */ }"
|
|
|
- />
|
|
|
- <p className="mt-2 text-xs text-gray-500">
|
|
|
- Use CSS custom properties like <code>var(--color-primary)</code> to reference theme colors
|
|
|
- </p>
|
|
|
+ {previewMode && (
|
|
|
+ <div className="theme-surface border theme-border rounded-lg p-4 mb-6">
|
|
|
+ <div className="flex items-center">
|
|
|
+ <svg
|
|
|
+ className="h-5 w-5 theme-primary"
|
|
|
+ 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 theme-text">
|
|
|
+ <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="theme-surface shadow rounded-lg">
|
|
|
+ <div className="px-6 py-4 border-b theme-border">
|
|
|
+ <h2 className="text-lg font-semibold theme-text">
|
|
|
+ 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 theme-text 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 theme-border 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 theme-text 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 theme-border 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 theme-text-secondary">
|
|
|
+ {isEditing
|
|
|
+ ? "ID cannot be changed after creation"
|
|
|
+ : "Unique identifier (lowercase, hyphens allowed)"}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Colors */}
|
|
|
+ <div className="theme-surface shadow rounded-lg">
|
|
|
+ <div className="px-6 py-4 border-b theme-border">
|
|
|
+ <h2 className="text-lg font-semibold theme-text">
|
|
|
+ Colors
|
|
|
+ </h2>
|
|
|
+ <p className="theme-text-secondary">
|
|
|
+ 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="theme-surface shadow rounded-lg">
|
|
|
+ <div className="px-6 py-4 border-b theme-border">
|
|
|
+ <h2 className="text-lg font-semibold theme-text">
|
|
|
+ Typography
|
|
|
+ </h2>
|
|
|
+ <p className="theme-text-secondary">
|
|
|
+ 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 theme-text 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 theme-border 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 theme-text 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 theme-border 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="theme-surface shadow rounded-lg">
|
|
|
+ <div className="px-6 py-4 border-b theme-border">
|
|
|
+ <h2 className="text-lg font-semibold theme-text">
|
|
|
+ Custom CSS
|
|
|
+ </h2>
|
|
|
+ <p className="theme-text-secondary">
|
|
|
+ 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 theme-border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm"
|
|
|
+ placeholder="/* Custom CSS rules */ .my-custom-class { /* your styles */ }"
|
|
|
+ />
|
|
|
+ <p className="mt-2 text-xs theme-text-secondary">
|
|
|
+ Use CSS custom properties like{" "}
|
|
|
+ <em>var(--color-primary)</em> 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 theme-border rounded-lg theme-text hover:theme-bg"
|
|
|
+ >
|
|
|
+ Cancel
|
|
|
+ </Link>
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ disabled={saving}
|
|
|
+ className="px-6 py-2 btn-theme-primary text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ >
|
|
|
+ {saving
|
|
|
+ ? "Saving..."
|
|
|
+ : isEditing
|
|
|
+ ? "Update Theme"
|
|
|
+ : "Create Theme"}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
</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>
|
|
|
- );
|
|
|
+ </div>
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
-export default ThemeEditor;
|
|
|
+export default ThemeEditor;
|