|
@@ -218,8 +218,21 @@ function PostEditor() {
|
|
|
}) : "";
|
|
}) : "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+ const [settingsOpen, setSettingsOpen] = useState(false);
|
|
|
|
|
+
|
|
|
|
|
+ // Auto-resize title textarea
|
|
|
|
|
+ const titleRef = React.useRef(null);
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ if (titleRef.current) {
|
|
|
|
|
+ titleRef.current.style.height = "auto";
|
|
|
|
|
+ titleRef.current.style.height = titleRef.current.scrollHeight + "px";
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [formData.title]);
|
|
|
|
|
+
|
|
|
|
|
+ // ... existing interactions ...
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
- <div className="min-h-screen theme-bg">
|
|
|
|
|
|
|
+ <div className="min-h-screen theme-bg font-sans theme-text selection:bg-blue-100 selection:text-blue-900 flex flex-col transition-colors duration-300">
|
|
|
<MediaGalleryModal
|
|
<MediaGalleryModal
|
|
|
isOpen={galleryOpen}
|
|
isOpen={galleryOpen}
|
|
|
onClose={() => setGalleryOpen(false)}
|
|
onClose={() => setGalleryOpen(false)}
|
|
@@ -227,345 +240,260 @@ function PostEditor() {
|
|
|
slug={getSlugForUpload()}
|
|
slug={getSlugForUpload()}
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <div className="max-w-7xl 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 Post" : "Create New Post"}
|
|
|
|
|
- </h1>
|
|
|
|
|
- <p className="theme-text-secondary">
|
|
|
|
|
- {isEditing
|
|
|
|
|
- ? "Update your existing post"
|
|
|
|
|
- : "Write a new blog post"}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="flex space-x-3">
|
|
|
|
|
- <Link
|
|
|
|
|
- to="/admin"
|
|
|
|
|
- className="btn-theme-secondary text-white px-4 py-2 rounded-lg transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- Back to Admin
|
|
|
|
|
- </Link>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* Glassmorphic Top Bar */}
|
|
|
|
|
+ <nav className="sticky top-0 z-40 bg-white/80 dark:bg-[#0d1117]/80 backdrop-blur-md border-b theme-border px-4 h-16 flex items-center justify-between transition-all duration-300">
|
|
|
|
|
+ <div className="flex items-center gap-4">
|
|
|
|
|
+ <Link
|
|
|
|
|
+ to="/admin"
|
|
|
|
|
+ className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors theme-text-secondary hover:theme-text"
|
|
|
|
|
+ title="Back to Admin"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /></svg>
|
|
|
|
|
+ </Link>
|
|
|
|
|
+ <span className="text-sm font-medium theme-text-secondary hidden sm:block">
|
|
|
|
|
+ {saving ? "Saving..." : isEditing ? "Editing" : "Drafting"}
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- {error && (
|
|
|
|
|
- <div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
|
|
|
|
|
- <div className="flex">
|
|
|
|
|
- <div className="flex-shrink-0">
|
|
|
|
|
- <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>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="ml-3">
|
|
|
|
|
- <h3 className="text-sm font-medium text-red-800">
|
|
|
|
|
- Error
|
|
|
|
|
- </h3>
|
|
|
|
|
- <p className="mt-1 text-sm text-red-700">
|
|
|
|
|
- {error}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
|
|
+ {/* View Toggle */}
|
|
|
|
|
+ <div className="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setActiveTab("write")}
|
|
|
|
|
+ className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${activeTab === "write"
|
|
|
|
|
+ ? "theme-surface theme-text shadow-sm"
|
|
|
|
|
+ : "theme-text-secondary hover:theme-text"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ Write
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setActiveTab("preview")}
|
|
|
|
|
+ className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${activeTab === "preview"
|
|
|
|
|
+ ? "theme-surface theme-text shadow-sm"
|
|
|
|
|
+ : "theme-text-secondary hover:theme-text"
|
|
|
|
|
+ }`}
|
|
|
|
|
+ >
|
|
|
|
|
+ Preview
|
|
|
|
|
+ </button>
|
|
|
</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">
|
|
|
|
|
- Post Information
|
|
|
|
|
- </h2>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <div className="h-6 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setSettingsOpen(true)}
|
|
|
|
|
+ className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 theme-text-secondary hover:theme-text transition-colors"
|
|
|
|
|
+ title="Post Settings"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" /></svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={handleSubmit}
|
|
|
|
|
+ disabled={saving}
|
|
|
|
|
+ className="bg-blue-600 hover:bg-blue-700 text-white px-5 py-2 rounded-lg font-medium text-sm transition-all shadow-sm hover:shadow active:scale-95 disabled:opacity-70 disabled:cursor-not-allowed"
|
|
|
|
|
+ >
|
|
|
|
|
+ {saving ? "Publishing..." : "Publish"}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </nav>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Main Content Area - Centered & Focused -> Increased max-width */}
|
|
|
|
|
+ <div className="flex-grow overflow-y-auto">
|
|
|
|
|
+ <div className="max-w-7xl mx-auto px-6 py-12 md:py-20 lg:py-24">
|
|
|
|
|
+ {error && (
|
|
|
|
|
+ <div className="mb-8 p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg flex items-center gap-3 animate-in fade-in slide-in-from-top-2">
|
|
|
|
|
+ <svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
|
|
|
|
+ {error}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-6">
|
|
|
|
|
+ {/* Title Input - Scaled down and boxed */}
|
|
|
|
|
+ <div className="border theme-border rounded-xl p-4 bg-gray-50/50 dark:bg-gray-800/50">
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ ref={titleRef}
|
|
|
|
|
+ value={formData.title}
|
|
|
|
|
+ onChange={(e) => handleInputChange("title", e.target.value)}
|
|
|
|
|
+ placeholder="Post Title"
|
|
|
|
|
+ rows={1}
|
|
|
|
|
+ className="w-full text-2xl md:text-3xl lg:text-4xl font-bold bg-transparent border-none placeholder-gray-400 dark:placeholder-gray-500 theme-text focus:ring-0 px-0 leading-tight resize-none overflow-hidden"
|
|
|
|
|
+ style={{ minHeight: '1.5em' }}
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="px-6 py-4 space-y-6">
|
|
|
|
|
- <div>
|
|
|
|
|
- <label
|
|
|
|
|
- htmlFor="title"
|
|
|
|
|
- className="block text-sm font-medium theme-text mb-1"
|
|
|
|
|
- >
|
|
|
|
|
- Title *
|
|
|
|
|
- </label>
|
|
|
|
|
- <input
|
|
|
|
|
- type="text"
|
|
|
|
|
- id="title"
|
|
|
|
|
- required
|
|
|
|
|
- value={formData.title}
|
|
|
|
|
- onChange={(e) =>
|
|
|
|
|
- handleInputChange(
|
|
|
|
|
- "title",
|
|
|
|
|
- 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="Enter post title..."
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- <div>
|
|
|
|
|
- <label
|
|
|
|
|
- htmlFor="description"
|
|
|
|
|
- className="block text-sm font-medium theme-text mb-1"
|
|
|
|
|
- >
|
|
|
|
|
- Description
|
|
|
|
|
- </label>
|
|
|
|
|
- <input
|
|
|
|
|
- type="text"
|
|
|
|
|
- id="description"
|
|
|
|
|
- value={formData.description}
|
|
|
|
|
- onChange={(e) =>
|
|
|
|
|
- handleInputChange(
|
|
|
|
|
- "description",
|
|
|
|
|
- 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="Short description or excerpt..."
|
|
|
|
|
|
|
+ {/* Description Input - Subtitle style */}
|
|
|
|
|
+ <div className="border theme-border rounded-xl p-4 bg-gray-50/50 dark:bg-gray-800/50">
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ value={formData.description}
|
|
|
|
|
+ onChange={(e) => handleInputChange("description", e.target.value)}
|
|
|
|
|
+ placeholder="Post Description (Subtitle)..."
|
|
|
|
|
+ rows={2}
|
|
|
|
|
+ className="w-full text-lg font-medium bg-transparent border-none placeholder-gray-400 dark:placeholder-gray-500 theme-text-secondary focus:ring-0 px-0 leading-relaxed resize-none overflow-hidden"
|
|
|
|
|
+ style={{ minHeight: '3em' }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {activeTab === "write" ? (
|
|
|
|
|
+ <div className="prose-editor-wrapper -mx-4 md:mx-0">
|
|
|
|
|
+ <MDEditor
|
|
|
|
|
+ value={formData.content}
|
|
|
|
|
+ onChange={(value) => handleInputChange("content", value || "")}
|
|
|
|
|
+ commands={[...commands.getCommands(), ...customCommands]}
|
|
|
|
|
+ height={window.innerHeight * 0.7} // Increased height to 70% of viewport
|
|
|
|
|
+ minHeight={500}
|
|
|
|
|
+ preview="edit"
|
|
|
|
|
+ visibleDragBar={false}
|
|
|
|
|
+ textareaProps={{
|
|
|
|
|
+ placeholder: "Tell your story...",
|
|
|
|
|
+ }}
|
|
|
|
|
+ className="shadow-sm" // Add slight shadow for separation
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <article className="markdown-content prose dark:prose-invert max-w-none prose-lg">
|
|
|
|
|
+ <div
|
|
|
|
|
+ dangerouslySetInnerHTML={{ __html: sanitizedPreview }}
|
|
|
|
|
+ ref={(node) => {
|
|
|
|
|
+ if (node) {
|
|
|
|
|
+ // Initialize comparison sliders if present in preview
|
|
|
|
|
+ const sliders = node.querySelectorAll(".comparison-slider");
|
|
|
|
|
+ sliders.forEach(slider => {
|
|
|
|
|
+ slider.oninput = (e) => {
|
|
|
|
|
+ const container = e.target.closest(".comparison-wrapper");
|
|
|
|
|
+ const topImage = container.querySelector(".comparison-top");
|
|
|
|
|
+ const handle = container.querySelector(".slider-handle");
|
|
|
|
|
+ const val = e.target.value;
|
|
|
|
|
+
|
|
|
|
|
+ if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`;
|
|
|
|
|
+ if (handle) handle.style.left = `${val}%`;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Initialize Zoom Reels in Preview (Re-using logic from PostView slightly simplified)
|
|
|
|
|
+ const zoomReels = node.querySelectorAll(".interactive-zoom-reel");
|
|
|
|
|
+ zoomReels.forEach(reel => {
|
|
|
|
|
+ const images = reel.querySelectorAll(".zoom-reel-img");
|
|
|
|
|
+ const viewports = reel.querySelectorAll(".zoom-reel-viewport");
|
|
|
|
|
+ const slider = reel.querySelector(".zoom-slider");
|
|
|
|
|
+ const resetBtn = reel.querySelector(".reset-zoom");
|
|
|
|
|
+
|
|
|
|
|
+ let state = { zoom: 1, panX: 0, panY: 0, isDragging: false, startX: 0, startY: 0, initialPanX: 0, initialPanY: 0 };
|
|
|
|
|
+ const updateTransform = () => {
|
|
|
|
|
+ images.forEach(img => {
|
|
|
|
|
+ img.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`;
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ slider.oninput = (e) => {
|
|
|
|
|
+ state.zoom = parseFloat(e.target.value);
|
|
|
|
|
+ if (state.zoom === 1) { state.panX = 0; state.panY = 0; }
|
|
|
|
|
+ updateTransform();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ resetBtn.onclick = () => {
|
|
|
|
|
+ state.zoom = 1; state.panX = 0; state.panY = 0; slider.value = 1;
|
|
|
|
|
+ updateTransform();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // Simplified drag for preview
|
|
|
|
|
+ reel.onmousedown = (e) => {
|
|
|
|
|
+ if (state.zoom <= 1) return;
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ state.isDragging = true;
|
|
|
|
|
+ state.startX = e.clientX;
|
|
|
|
|
+ state.startY = e.clientY;
|
|
|
|
|
+ state.initialPanX = state.panX;
|
|
|
|
|
+ state.initialPanY = state.panY;
|
|
|
|
|
+ viewports.forEach(v => v.style.cursor = "grabbing");
|
|
|
|
|
+ };
|
|
|
|
|
+ reel.onmousemove = (e) => {
|
|
|
|
|
+ if (!state.isDragging) return;
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const dx = e.clientX - state.startX;
|
|
|
|
|
+ const dy = e.clientY - state.startY;
|
|
|
|
|
+ state.panX = state.initialPanX + dx;
|
|
|
|
|
+ state.panY = state.initialPanY + dy;
|
|
|
|
|
+ updateTransform();
|
|
|
|
|
+ };
|
|
|
|
|
+ reel.onmouseup = () => {
|
|
|
|
|
+ state.isDragging = false;
|
|
|
|
|
+ viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
+ };
|
|
|
|
|
+ reel.onmouseleave = () => {
|
|
|
|
|
+ state.isDragging = false;
|
|
|
|
|
+ viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </article>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Settings Drawer - Slide-out */}
|
|
|
|
|
+ {settingsOpen && (
|
|
|
|
|
+ <div className="fixed inset-0 z-50 overflow-hidden">
|
|
|
|
|
+ <div className="absolute inset-0 bg-black/20 backdrop-blur-sm transition-opacity" onClick={() => setSettingsOpen(false)}></div>
|
|
|
|
|
+ <div className="absolute inset-y-0 right-0 max-w-xs w-full theme-surface shadow-2xl transform transition-transform duration-300 ease-in-out px-6 py-6 border-l theme-border flex flex-col h-full animate-in slide-in-from-right">
|
|
|
|
|
+
|
|
|
|
|
+ <div className="flex items-center justify-between mb-8">
|
|
|
|
|
+ <h2 className="text-lg font-bold theme-text">Post Settings</h2>
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={() => setSettingsOpen(false)}
|
|
|
|
|
+ className="p-2 -mr-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 theme-text-secondary hover:theme-text"
|
|
|
|
|
+ >
|
|
|
|
|
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-6 flex-grow overflow-y-auto">
|
|
|
|
|
+
|
|
|
|
|
|
|
|
<div>
|
|
<div>
|
|
|
- <label
|
|
|
|
|
- htmlFor="tags"
|
|
|
|
|
- className="block text-sm font-medium theme-text mb-1"
|
|
|
|
|
- >
|
|
|
|
|
- Tags
|
|
|
|
|
- </label>
|
|
|
|
|
|
|
+ <label htmlFor="tags" className="block text-xs font-bold uppercase tracking-wider theme-text-secondary mb-2">Tags</label>
|
|
|
<input
|
|
<input
|
|
|
type="text"
|
|
type="text"
|
|
|
id="tags"
|
|
id="tags"
|
|
|
value={formData.tags}
|
|
value={formData.tags}
|
|
|
- onChange={(e) =>
|
|
|
|
|
- handleInputChange(
|
|
|
|
|
- "tags",
|
|
|
|
|
- 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="tag1, tag2, tag3..."
|
|
|
|
|
|
|
+ onChange={(e) => handleInputChange("tags", e.target.value)}
|
|
|
|
|
+ className="w-full bg-transparent border theme-border rounded-lg px-4 py-3 text-sm focus:ring-2 focus:ring-blue-500/20 outline-none transition-all theme-text"
|
|
|
|
|
+ placeholder="technology, life..."
|
|
|
/>
|
|
/>
|
|
|
- <p className="mt-1 text-sm theme-text-secondary">
|
|
|
|
|
- Separate tags with commas
|
|
|
|
|
- </p>
|
|
|
|
|
|
|
+ <p className="mt-2 text-xs theme-text-secondary">Comma separated.</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div className="flex items-center">
|
|
|
|
|
- <input
|
|
|
|
|
- type="checkbox"
|
|
|
|
|
- id="hidden"
|
|
|
|
|
- checked={formData.hidden}
|
|
|
|
|
- onChange={(e) =>
|
|
|
|
|
- handleInputChange(
|
|
|
|
|
- "hidden",
|
|
|
|
|
- e.target.checked
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
|
|
|
- />
|
|
|
|
|
- <label
|
|
|
|
|
- htmlFor="hidden"
|
|
|
|
|
- className="ml-2 block text-sm theme-text"
|
|
|
|
|
- >
|
|
|
|
|
- Admin Only (Hidden from public)
|
|
|
|
|
|
|
+ <div className="pt-4 border-t theme-border">
|
|
|
|
|
+ <label className="flex items-center gap-3 cursor-pointer group">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ checked={formData.hidden}
|
|
|
|
|
+ onChange={(e) => handleInputChange("hidden", e.target.checked)}
|
|
|
|
|
+ className="w-5 h-5 theme-text rounded focus:ring-gray-500 border-gray-300 transition-colors"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div className="flex flex-col">
|
|
|
|
|
+ <span className="text-sm font-medium theme-text group-hover:theme-text transition-colors">Hidden Post</span>
|
|
|
|
|
+ <span className="text-xs theme-text-secondary">Only visible to admins</span>
|
|
|
|
|
+ </div>
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
|
|
|
|
- {/* Content Editor */}
|
|
|
|
|
- <div className="theme-surface shadow rounded-lg">
|
|
|
|
|
- <div className="px-6 py-4 border-b theme-border flex flex-col sm:flex-row justify-between items-center space-y-3 sm:space-y-0">
|
|
|
|
|
- <h2 className="text-lg font-semibold theme-text">
|
|
|
|
|
- Content
|
|
|
|
|
- </h2>
|
|
|
|
|
- <div className="flex space-x-4">
|
|
|
|
|
- {/* Custom Tabs */}
|
|
|
|
|
- <div className="flex space-x-2 bg-gray-100 p-1 rounded-lg border theme-border">
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={() => setActiveTab("write")}
|
|
|
|
|
- className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${activeTab === "write"
|
|
|
|
|
- ? "bg-white text-blue-600 shadow-sm"
|
|
|
|
|
- : "text-gray-500 hover:text-gray-700"
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- Write
|
|
|
|
|
- </button>
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- onClick={() => setActiveTab("preview")}
|
|
|
|
|
- className={`px-4 py-1.5 rounded-md text-sm font-medium transition-all ${activeTab === "preview"
|
|
|
|
|
- ? "bg-white text-blue-600 shadow-sm"
|
|
|
|
|
- : "text-gray-500 hover:text-gray-700"
|
|
|
|
|
- }`}
|
|
|
|
|
- >
|
|
|
|
|
- Preview
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div className="mt-auto pt-6 border-t theme-border">
|
|
|
|
|
+ <div className="text-xs theme-text-secondary text-center">
|
|
|
|
|
+ Settings allow you to fine-tune metadata.
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <div className="px-6 py-4">
|
|
|
|
|
- <div className="border theme-border rounded-lg overflow-hidden min-h-[500px]">
|
|
|
|
|
- {activeTab === "write" ? (
|
|
|
|
|
- <MDEditor
|
|
|
|
|
- value={formData.content}
|
|
|
|
|
- onChange={(value) =>
|
|
|
|
|
- handleInputChange(
|
|
|
|
|
- "content",
|
|
|
|
|
- value || "",
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- commands={[...commands.getCommands(), ...customCommands]}
|
|
|
|
|
- height={500}
|
|
|
|
|
- preview="edit" // Hide default preview
|
|
|
|
|
- hideToolbar={false}
|
|
|
|
|
- visibleDragBar={false}
|
|
|
|
|
- textareaProps={{
|
|
|
|
|
- placeholder:
|
|
|
|
|
- "Write your post content in Markdown...",
|
|
|
|
|
- style: {
|
|
|
|
|
- fontSize: "14px",
|
|
|
|
|
- fontFamily:
|
|
|
|
|
- "ui-monospace, monospace",
|
|
|
|
|
- },
|
|
|
|
|
- required: true,
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <div
|
|
|
|
|
- className="p-8 bg-white markdown-content"
|
|
|
|
|
- dangerouslySetInnerHTML={{ __html: sanitizedPreview }}
|
|
|
|
|
- ref={(node) => {
|
|
|
|
|
- if (node) {
|
|
|
|
|
- // Initialize comparison sliders if present in preview
|
|
|
|
|
- const sliders = node.querySelectorAll(".comparison-slider");
|
|
|
|
|
- sliders.forEach(slider => {
|
|
|
|
|
- slider.oninput = (e) => {
|
|
|
|
|
- const container = e.target.closest(".comparison-wrapper");
|
|
|
|
|
- const topImage = container.querySelector(".comparison-top");
|
|
|
|
|
- const handle = container.querySelector(".slider-handle");
|
|
|
|
|
- const val = e.target.value;
|
|
|
|
|
-
|
|
|
|
|
- if (topImage) topImage.style.clipPath = `inset(0 ${100 - val}% 0 0)`;
|
|
|
|
|
- if (handle) handle.style.left = `${val}%`;
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Initialize Zoom Reels in Preview
|
|
|
|
|
- const zoomReels = node.querySelectorAll(".interactive-zoom-reel");
|
|
|
|
|
- zoomReels.forEach(reel => {
|
|
|
|
|
- // Cleanup old listeners if any (hard in a ref callback without cleanup hook,
|
|
|
|
|
- // but preview re-renders fully so nodes are new).
|
|
|
|
|
- const images = reel.querySelectorAll(".zoom-reel-img");
|
|
|
|
|
- const viewports = reel.querySelectorAll(".zoom-reel-viewport");
|
|
|
|
|
- const slider = reel.querySelector(".zoom-slider");
|
|
|
|
|
- const resetBtn = reel.querySelector(".reset-zoom");
|
|
|
|
|
-
|
|
|
|
|
- let state = { zoom: 1, panX: 0, panY: 0, isDragging: false, startX: 0, startY: 0, initialPanX: 0, initialPanY: 0 };
|
|
|
|
|
- const updateTransform = () => {
|
|
|
|
|
- images.forEach(img => {
|
|
|
|
|
- img.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.zoom})`;
|
|
|
|
|
- });
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- slider.oninput = (e) => {
|
|
|
|
|
- state.zoom = parseFloat(e.target.value);
|
|
|
|
|
- if (state.zoom === 1) { state.panX = 0; state.panY = 0; }
|
|
|
|
|
- updateTransform();
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- resetBtn.onclick = () => {
|
|
|
|
|
- state.zoom = 1; state.panX = 0; state.panY = 0; slider.value = 1;
|
|
|
|
|
- updateTransform();
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- viewports.forEach(vp => {
|
|
|
|
|
- vp.onmousedown = (e) => {
|
|
|
|
|
- if (state.zoom <= 1) return;
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- state.isDragging = true;
|
|
|
|
|
- state.startX = e.clientX;
|
|
|
|
|
- state.startY = e.clientY;
|
|
|
|
|
- state.initialPanX = state.panX;
|
|
|
|
|
- state.initialPanY = state.panY;
|
|
|
|
|
- viewports.forEach(v => v.style.cursor = "grabbing");
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- // Note: mousemove/up on document won't easily work scoped here without leaking listeners
|
|
|
|
|
- // or extensive cleanup logic which Ref callback doesn't support well.
|
|
|
|
|
- // RESTRICTION: In Preview, drag might only work if mouse stays over the element if we attach to node,
|
|
|
|
|
- // or we accept leak (bad).
|
|
|
|
|
- // Compromise: Attach to 'reel' valid for preview.
|
|
|
|
|
- reel.onmousemove = (e) => {
|
|
|
|
|
- if (!state.isDragging) return;
|
|
|
|
|
- e.preventDefault();
|
|
|
|
|
- const dx = e.clientX - state.startX;
|
|
|
|
|
- const dy = e.clientY - state.startY;
|
|
|
|
|
- state.panX = state.initialPanX + dx;
|
|
|
|
|
- state.panY = state.initialPanY + dy;
|
|
|
|
|
- updateTransform();
|
|
|
|
|
- }
|
|
|
|
|
- reel.onmouseup = () => {
|
|
|
|
|
- state.isDragging = false;
|
|
|
|
|
- viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
- };
|
|
|
|
|
- reel.onmouseleave = () => {
|
|
|
|
|
- state.isDragging = false;
|
|
|
|
|
- viewports.forEach(v => v.style.cursor = "grab");
|
|
|
|
|
- };
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- }}
|
|
|
|
|
- />
|
|
|
|
|
- )}
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div className="mt-3 text-xs theme-text-secondary bg-blue-50 p-3 rounded-lg">
|
|
|
|
|
- <p><strong>Tips:</strong></p>
|
|
|
|
|
- <ul className="list-disc list-inside mt-1 space-y-1">
|
|
|
|
|
- <li><strong>Media:</strong> Use the Media button to upload images.</li>
|
|
|
|
|
- <li><strong>Zoom Reel:</strong> <code>```zoom-reel</code> block with images. Synchronized pan/zoom.</li>
|
|
|
|
|
- <li><strong>Comparison:</strong> Custom <code>```compare</code> block with 2 images inside.</li>
|
|
|
|
|
- <li><strong>Image Resize:</strong> <code></code></li>
|
|
|
|
|
- </ul>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
- {/* Actions */}
|
|
|
|
|
- <div className="flex justify-end space-x-4">
|
|
|
|
|
- <Link
|
|
|
|
|
- to="/admin"
|
|
|
|
|
- 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 Post"
|
|
|
|
|
- : "Create Post"}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </form>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|