فهرست منبع

refactor: redesign post editor interface with improved styling

Adam Jafarov 3 هفته پیش
والد
کامیت
69313652c2
2فایلهای تغییر یافته به همراه195 افزوده شده و 135 حذف شده
  1. 183 134
      src/components/PostEditor.jsx
  2. 12 1
      src/index.css

+ 183 - 134
src/components/PostEditor.jsx

@@ -94,16 +94,7 @@ function PostEditor() {
     };
 
     // Improved cursor tracker
-    const handleEditorChange = (value, event, state) => {
-        handleInputChange("content", value || "");
 
-        // Calculate line number from selection start
-        // MDEditor expose state with selection
-        if (state && state.selection && state.selection.start) {
-            const lines = value.substr(0, state.selection.start).split("\n");
-            setCursorLine(lines.length);
-        }
-    };
 
     const insertTextAtCursor = (text) => {
         setFormData(prev => ({
@@ -112,6 +103,22 @@ function PostEditor() {
         }));
     };
 
+    // Improved cursor tracker
+    const handleCursorSelect = (e) => {
+        // e.target is the textarea
+        if (!e || !e.target) return;
+        const { selectionStart, value } = e.target;
+        const lines = value.substr(0, selectionStart).split("\n");
+        // Markdown-it lines are 0-based. lines.length is 1-based count.
+        // So line index is lines.length - 1.
+        setCursorLine(lines.length - 1);
+    };
+
+    const handleEditorChange = (value, event, state) => {
+        handleInputChange("content", value || "");
+        // We rely on handleCursorSelect via onSelect/onClick/onKeyUp for cursor tracking now.
+    };
+
     const handlePaste = async (event) => {
         const items = event.clipboardData.items;
         for (const item of items) {
@@ -283,16 +290,22 @@ function PostEditor() {
         }
     };
 
+    const [viewMode, setViewMode] = useState("tabs"); // "tabs" | "split"
+
     // Process markdown for preview
     const renderContent = () => {
         return md.render(formData.content || "");
     };
 
-    const previewHtml = activeTab === "preview" ? renderContent() : "";
-    const sanitizedPreview = activeTab === "preview" ? DOMPurify.sanitize(previewHtml, {
+    // Calculate preview for both modes
+    // Helper to sanitize
+    const getSanitizedHtml = (html) => DOMPurify.sanitize(html, {
         ADD_TAGS: ["input"],
-        ADD_ATTR: ["type", "min", "max", "value", "step", "style", "width", "height", "class"],
-    }) : "";
+        ADD_ATTR: ["type", "min", "max", "value", "step", "style", "width", "height", "class", "data-line"], // Added data-line
+    });
+
+    const previewHtml = renderContent();
+    const sanitizedPreview = getSanitizedHtml(previewHtml);
 
 
     const [settingsOpen, setSettingsOpen] = useState(false);
@@ -306,11 +319,10 @@ function PostEditor() {
         }
     }, [formData.title]);
 
-    // Scroll Sync Effect
+
+    // Scroll Sync & Flicker Effect
     useEffect(() => {
-        if (activeTab === "preview") {
-            // Find the element with the closest data-line attribute
-            // We search for elements with data-line <= cursorLine, picking the largest one
+        if (activeTab === "preview" || viewMode === "split") {
             const elements = document.querySelectorAll('[data-line]');
             let target = null;
             let maxLine = -1;
@@ -325,11 +337,15 @@ function PostEditor() {
 
             if (target) {
                 target.scrollIntoView({ behavior: 'smooth', block: 'center' });
+
+                // Add flicker effect
+                target.classList.add('highlight-pulse');
+                setTimeout(() => {
+                    target.classList.remove('highlight-pulse');
+                }, 1500);
             }
         }
-    }, [activeTab]);
-
-    // ... existing interactions ...
+    }, [activeTab, cursorLine, viewMode]);
 
     return (
         <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">
@@ -356,30 +372,48 @@ function PostEditor() {
                 </div>
 
                 <div className="flex items-center gap-3">
-                    {/* View Toggle */}
-                    <div className="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg">
+                    {/* View Mode Toggle */}
+                    <div className="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg mr-2">
                         <button
                             type="button"
-                            onClick={() => setActiveTab("write")}
-                            className={`px-3 py-1.5 rounded-md text-xs font-semibold transition-all ${activeTab === "write"
+                            onClick={() => setViewMode(viewMode === "tabs" ? "split" : "tabs")}
+                            className={`px-2 py-1.5 rounded-md text-xs font-semibold transition-all flex items-center gap-1 ${viewMode === "split"
                                 ? "theme-surface theme-text shadow-sm"
                                 : "theme-text-secondary hover:theme-text"
                                 }`}
+                            title="Toggle Split View"
                         >
-                            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
+                            <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m-4 10a2 2 0 002 2h2a2 2 0 002-2m0 0V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /></svg>
+                            <span className="hidden sm:inline">Split</span>
                         </button>
                     </div>
 
+                    {/* Tab Toggles (Only visible in Tabs mode) */}
+                    {viewMode === "tabs" && (
+                        <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 className="h-6 w-px bg-gray-200 dark:bg-gray-700 mx-1"></div>
 
                     <button
@@ -401,9 +435,9 @@ function PostEditor() {
                 </div>
             </nav>
 
-            {/* Main Content Area - Centered & Focused -> Increased max-width */}
+            {/* Main Content Area */}
             <div className="flex-grow overflow-y-auto">
-                <div className="max-w-7xl mx-auto px-6 py-12 md:py-20 lg:py-24">
+                <div className={`max-w-7xl mx-auto px-6 py-12 md:py-20 lg:py-24 ${viewMode === "split" ? "max-w-[95vw]" : ""}`}>
                     {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>
@@ -436,104 +470,119 @@ function PostEditor() {
                                 style={{ minHeight: '3em' }}
                             />
                         </div>
-
-                        {activeTab === "write" ? (
-                            <div className="prose-editor-wrapper -mx-4 md:mx-0">
-                                <MDEditor
-                                    value={formData.content}
-                                    onChange={handleEditorChange}
-                                    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...",
-                                        onPaste: handlePaste
-                                    }}
-                                    className="shadow-sm" // Add slight shadow for separation
-                                />
-                            </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");
-                                                };
-                                            });
-                                        }
+                        {/* Split Editor/Preview Layout */}
+                        <div className={`grid gap-6 ${viewMode === "split" ? "grid-cols-1 lg:grid-cols-2" : "grid-cols-1"}`}>
+
+                            {/* Editor Column - Visible if write tab active OR split mode */}
+                            {activeTab === "write" || viewMode === "split" ? (
+                                <div className="prose-editor-wrapper -mx-4 md:mx-0">
+                                    <MDEditor
+                                        value={formData.content}
+                                        onChange={handleEditorChange}
+                                        commands={[...commands.getCommands(), ...customCommands]}
+                                        height={viewMode === "split" ? window.innerHeight * 0.8 : window.innerHeight * 0.7}
+                                        minHeight={500}
+                                        preview="edit"
+                                        visibleDragBar={false}
+                                        textareaProps={{
+                                            placeholder: "Tell your story...",
+                                            onPaste: handlePaste,
+                                            onSelect: handleCursorSelect,
+                                            onClick: handleCursorSelect,
+                                            onKeyUp: handleCursorSelect
+                                        }}
+                                        className="shadow-sm sticky top-24"
+                                    />
+                                </div>
+                            ) : null}
+
+                            {/* Preview Column - Visible if preview tab active OR split mode */}
+                            {(activeTab === "preview" || viewMode === "split") && (
+                                <article
+                                    className={`markdown-content prose dark:prose-invert max-w-none prose-lg ${viewMode === "split" ? "border-l pl-6 border-gray-200 dark:border-gray-800" : ""}`}
+                                    style={{
+                                        height: viewMode === "split" ? window.innerHeight * 0.8 : 'auto',
+                                        overflowY: viewMode === "split" ? 'auto' : 'visible'
                                     }}
-                                />
-                            </article>
-                        )}
+                                >
+                                    <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>
             </div>

+ 12 - 1
src/index.css

@@ -637,4 +637,15 @@ body {
         transform: scale(1);
         opacity: 1;
     }
-}
+}
+/* Flicker Animation for Scroll Sync */
+@keyframes highlight-pulse-animation {
+  0% { background-color: rgba(59, 130, 246, 0.1); }
+  50% { background-color: rgba(59, 130, 246, 0.3); }
+  100% { background-color: transparent; }
+}
+
+.highlight-pulse {
+  animation: highlight-pulse-animation 1.5s ease-out;
+  border-radius: 4px;
+}