| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653 |
- import React, { useState, useEffect, useMemo } from "react";
- import { useNavigate, useParams, Link } from "react-router-dom";
- import MDEditor, { commands } from "@uiw/react-md-editor";
- import "@uiw/react-md-editor/markdown-editor.css";
- import { API_BASE } from "../config";
- import { createMarkdownParser } from "../utils/markdownParser";
- import DOMPurify from "dompurify";
- import MediaGalleryModal from "./MediaGalleryModal";
- // Initialize the shared markdown parser
- const md = createMarkdownParser();
- function PostEditor() {
- const navigate = useNavigate();
- const { slug } = useParams();
- const isEditing = !!slug;
- const [activeTab, setActiveTab] = useState("write"); // "write" | "preview"
- const [galleryOpen, setGalleryOpen] = useState(false);
- const [formData, setFormData] = useState({
- title: "",
- description: "",
- content: "",
- tags: "",
- hidden: false,
- });
- const [cursorLine, setCursorLine] = useState(0);
- const [loading, setLoading] = useState(isEditing);
- const [saving, setSaving] = useState(false);
- const [error, setError] = useState(null);
- // Calculate temporary slug for new posts
- const getSlugForUpload = () => {
- if (isEditing) return slug;
- if (formData.title) {
- const date = new Date();
- const dateStr = date.toISOString().slice(0, 10).replace(/-/g, "");
- const slugText = formData.title
- .toLowerCase()
- .replace(/[^a-z0-9]+/g, "-")
- .replace(/^-|-$/g, "")
- .slice(0, 30);
- return `${dateStr}-${slugText}`;
- }
- return "";
- };
- useEffect(() => {
- if (isEditing) {
- fetchPost();
- }
- }, [slug, isEditing]);
- const fetchPost = async () => {
- try {
- setLoading(true);
- const response = await fetch(`${API_BASE}/posts/${slug}`, {
- credentials: "include",
- });
- if (!response.ok) throw new Error("Failed to fetch post");
- const post = await response.json();
- let content = post.content;
- content = content.replace(/^title:.*$/m, "");
- content = content.replace(/^desc:.*$/m, "");
- content = content.replace(/^tags:.*$/m, "");
- content = content.replace(/^hidden:.*$/m, ""); // Remove hidden if present in content body (should be only in header but just in case)
- content = content.replace(/^\n+/, "");
- setFormData({
- title: post.title,
- description: post.description,
- content: content.trim(),
- tags: post.tags ? post.tags.join(", ") : "",
- hidden: !!post.hidden,
- });
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- };
- const handleInputChange = (field, value, event) => {
- setFormData((prev) => ({ ...prev, [field]: value }));
- // Track cursor line for sync
- if (field === "content" && event) {
- // MDEditor's onChange passes the value, but we need the event or access to the textarea
- // However, uiw/react-md-editor's onChange only gives value.
- // We need to use onSelect or capture it from the underlying textarea.
- }
- };
- // Improved cursor tracker
- const insertTextAtCursor = (text) => {
- setFormData(prev => ({
- ...prev,
- content: prev.content + "\n" + text
- }));
- };
- // 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) {
- if (item.type.indexOf("image") === 0) {
- event.preventDefault();
- const file = item.getAsFile();
- if (!file) continue;
- const tempSlug = getSlugForUpload() || "uploads";
- const placeholder = `![Uploading ${file.name}...]()...`;
- // Insert placeholder
- // We use api.replaceSelection if we had access to 'api' from MDEditor,
- // but here we are in a raw paste handler on the textarea.
- // We'll append for now or try to use a more sophisticated insertion if possible,
- // but sticking to insertTextAtCursor is safer for state consistency.
- // BETTER: MDEditor's `paste` command? No, native onPaste is best.
- // Native paste on textarea doesn't give us cursor position easily in React state flow
- // without ref manipulation.
- // Let's use the standard "append" or try to insert at end.
- // Actually, let's use the replaceSelection approach if we can get a ref to the editor instance,
- // but we don't have it easily.
- // We will append to content with a newline for simplicity and reliability.
- setFormData(prev => ({ ...prev, content: prev.content + "\n" + placeholder }));
- try {
- const formData = new FormData();
- formData.append("file", file);
- const response = await fetch(`${API_BASE}/upload?slug=${tempSlug}`, {
- method: "POST",
- body: formData,
- credentials: "include",
- });
- if (!response.ok) throw new Error("Upload failed");
- const data = await response.json();
- // Replace placeholder with actual image markdown
- setFormData(prev => ({
- ...prev,
- content: prev.content.replace(placeholder, ``)
- }));
- } catch (error) {
- console.error("Paste upload error:", error);
- setFormData(prev => ({
- ...prev,
- content: prev.content.replace(placeholder, `[Upload Failed: ${error.message}]`)
- }));
- }
- }
- }
- };
- // Gallery selection handler
- const handleImageSelect = (url, name) => {
- // We append to end since we lose cursor position when modal opens/closes
- // Ideally we would insert at cursor, but standard 'insertTextAtCursor' used state append.
- // Users can cut/paste.
- const markdown = ``;
- insertTextAtCursor(markdown);
- setGalleryOpen(false);
- };
- // Define Custom Toolbar Commands
- const customCommands = useMemo(() => {
- const mediaCommand = {
- name: "media",
- keyCommand: "media",
- buttonProps: { "aria-label": "Media Gallery", title: "Media Gallery" },
- icon: (
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
- ),
- execute: (state, api) => {
- setGalleryOpen(true);
- },
- };
- const compareCommand = {
- name: "compare",
- keyCommand: "compare",
- buttonProps: { "aria-label": "Insert Comparison", title: "Insert Comparison" },
- icon: (
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" /></svg>
- ),
- execute: (state, api) => {
- api.replaceSelection(`\n\`\`\`compare\n\n\n\`\`\`\n`);
- },
- };
- const reelCommand = {
- name: "reel",
- keyCommand: "reel",
- buttonProps: { "aria-label": "Insert Zoom Reel", title: "Insert Zoom Reel" },
- icon: (
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" /></svg>
- ),
- execute: (state, api) => {
- api.replaceSelection(`\n\`\`\`zoom-reel\n\n\n\`\`\`\n`);
- },
- };
- const resizeCommand = {
- name: "resize",
- keyCommand: "resize",
- buttonProps: { "aria-label": "Insert Resized Image", title: "Insert Resized Image" },
- icon: (
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
- ),
- execute: (state, api) => {
- api.replaceSelection(``);
- },
- };
- return [mediaCommand, compareCommand, reelCommand, resizeCommand];
- }, []);
- const handleSubmit = async (e) => {
- e.preventDefault();
- if (!formData.title.trim() || !formData.content.trim()) {
- setError("Title and content are required");
- return;
- }
- try {
- setSaving(true);
- setError(null);
- const payload = {
- title: formData.title.trim(),
- description: formData.description.trim(),
- content: formData.content.trim(),
- tags: formData.tags
- .split(",")
- .map((tag) => tag.trim())
- .filter((tag) => tag),
- hidden: formData.hidden,
- };
- const url = isEditing
- ? `${API_BASE}/posts/${slug}`
- : `${API_BASE}/posts`;
- const method = isEditing ? "PUT" : "POST";
- const response = await fetch(url, {
- method,
- headers: {
- "Content-Type": "application/json",
- },
- credentials: "include",
- body: JSON.stringify(payload),
- });
- if (!response.ok) {
- const errorData = await response.json();
- throw new Error(errorData.error || "Failed to save post");
- }
- const savedPost = await response.json();
- navigate(`/admin`);
- } catch (err) {
- setError(err.message);
- } finally {
- setSaving(false);
- }
- };
- const [viewMode, setViewMode] = useState("tabs"); // "tabs" | "split"
- // Process markdown for preview
- const renderContent = () => {
- return md.render(formData.content || "");
- };
- // 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", "data-line"], // Added data-line
- });
- const previewHtml = renderContent();
- const sanitizedPreview = getSanitizedHtml(previewHtml);
- 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]);
- // Scroll Sync & Flicker Effect
- useEffect(() => {
- if (activeTab === "preview" || viewMode === "split") {
- const elements = document.querySelectorAll('[data-line]');
- let target = null;
- let maxLine = -1;
- elements.forEach(el => {
- const line = parseInt(el.getAttribute('data-line'), 10);
- if (line <= cursorLine && line > maxLine) {
- maxLine = line;
- target = el;
- }
- });
- if (target) {
- target.scrollIntoView({ behavior: 'smooth', block: 'center' });
- // Add flicker effect
- target.classList.add('highlight-pulse');
- setTimeout(() => {
- target.classList.remove('highlight-pulse');
- }, 1500);
- }
- }
- }, [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">
- <MediaGalleryModal
- isOpen={galleryOpen}
- onClose={() => setGalleryOpen(false)}
- onSelect={handleImageSelect}
- slug={getSlugForUpload()}
- />
- {/* 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 className="flex items-center gap-3">
- {/* View Mode Toggle */}
- <div className="flex bg-gray-100 dark:bg-gray-800 p-1 rounded-lg mr-2">
- <button
- type="button"
- 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"
- >
- <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
- 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 */}
- <div className="flex-grow overflow-y-auto">
- <div className={`max-w-7xl mx-auto px-6 py-8 md:py-12 lg:py-16 ${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>
- {error}
- </div>
- )}
- <div className="space-y-6">
- {/* Title Input - Scaled down and boxed */}
- <a>Title:</a>
- <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-lg 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>
- {/* Description Input - Subtitle style */}
- <a>Description:</a>
- <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-base 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>
- {/* 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"
- />
- </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'
- }}
- >
- <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>
- {/* 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>
- <label htmlFor="tags" className="block text-xs font-bold uppercase tracking-wider theme-text-secondary mb-2">Tags</label>
- <input
- type="text"
- id="tags"
- value={formData.tags}
- 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-2 text-xs theme-text-secondary">Comma separated.</p>
- </div>
- <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>
- </div>
- </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>
- );
- }
- export default PostEditor;
|