"use client"; import React, { useState, useEffect, useRef, useCallback, Suspense, } from "react"; import { useSearchParams } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import ReactMarkdown from "react-markdown"; import { ArrowLeft, Save, Eye, X, Bold, Italic, Code, Image, Link, List, ListOrdered, Quote, Hash, Loader2, ExternalLink, Github, Tag, } from "lucide-react"; import { useToast } from "@/components/Toast"; interface Project { id: string; title: string; description: string; content?: string; category: string; tags?: string[]; featured: boolean; published: boolean; github?: string; live?: string; image?: string; createdAt: string; updatedAt: string; } function EditorPageContent() { const searchParams = useSearchParams(); const projectId = searchParams.get("id"); const initialLocale = searchParams.get("locale") || "en"; const contentRef = useRef(null); const { showSuccess, showError } = useToast(); const [, setProject] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [isCreating, setIsCreating] = useState(!projectId); const [editLocale, setEditLocale] = useState(initialLocale); const [baseTexts, setBaseTexts] = useState<{ title: string; description: string } | null>(null); const [showPreview, setShowPreview] = useState(false); const [_isTyping, setIsTyping] = useState(false); const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); const [originalFormData, setOriginalFormData] = useState(null); const shouldUpdateContentRef = useRef(true); // Form state const [formData, setFormData] = useState({ title: "", description: "", content: "", category: "web", tags: [] as string[], featured: false, published: false, github: "", live: "", image: "", }); const loadProject = useCallback(async (id: string) => { try { const response = await fetch("/api/projects"); if (response.ok) { const data = await response.json(); const foundProject = data.projects.find( (p: Project) => p.id.toString() === id, ); if (foundProject) { setBaseTexts({ title: foundProject.title || "", description: foundProject.description || "", }); const initialData = { title: foundProject.title || "", description: foundProject.description || "", content: foundProject.content || "", category: foundProject.category || "web", tags: foundProject.tags || [], featured: foundProject.featured || false, published: foundProject.published || false, github: foundProject.github || "", live: foundProject.live || "", image: foundProject.image || "", }; setProject(foundProject); setFormData(initialData); setOriginalFormData(initialData); setHistory([initialData]); setHistoryIndex(0); shouldUpdateContentRef.current = true; // Initialize contentEditable after state update setTimeout(() => { if (contentRef.current && initialData.content) { contentRef.current.textContent = initialData.content; } }, 0); } } else { if (process.env.NODE_ENV === "development") { console.error("Failed to fetch projects:", response.status); } } } catch (error) { if (process.env.NODE_ENV === "development") { console.error("Error loading project:", error); } } }, []); const loadTranslation = useCallback(async (id: string, locale: string) => { if (!id || !locale || locale === "en") return; try { const response = await fetch(`/api/projects/${id}/translation?locale=${encodeURIComponent(locale)}`, { headers: { "x-admin-request": "true", "x-session-token": sessionStorage.getItem("admin_session_token") || "", }, }); if (!response.ok) return; const data = await response.json(); const tr = data.translation as { title?: string; description?: string } | null; if (tr?.title && tr?.description) { setFormData((prev) => ({ ...prev, title: tr.title || prev.title, description: tr.description || prev.description, })); } } catch { // ignore translation load failures } }, []); // Check authentication and load project useEffect(() => { const init = async () => { try { // Check auth const authStatus = sessionStorage.getItem("admin_authenticated"); const sessionToken = sessionStorage.getItem("admin_session_token"); if (authStatus === "true" && sessionToken) { setIsAuthenticated(true); // Load project if editing if (projectId) { await loadProject(projectId); await loadTranslation(projectId, editLocale); } else { setIsCreating(true); // Initialize history for new project const initialData = { title: "", description: "", content: "", category: "web", tags: [], featured: false, published: false, github: "", live: "", image: "", }; setFormData(initialData); setOriginalFormData(initialData); setHistory([initialData]); setHistoryIndex(0); shouldUpdateContentRef.current = true; // Initialize contentEditable after state update setTimeout(() => { if (contentRef.current) { contentRef.current.textContent = ""; } }, 0); } } else { setIsAuthenticated(false); } } catch (error) { if (process.env.NODE_ENV === "development") { console.error("Error in init:", error); } setIsAuthenticated(false); } finally { setIsLoading(false); } }; init(); }, [projectId, loadProject, loadTranslation, editLocale]); const handleSave = useCallback(async () => { try { setIsSaving(true); // Validate required fields if (!formData.title.trim()) { showError("Validation Error", "Please enter a project title"); setIsSaving(false); return; } if (!formData.description.trim()) { showError("Validation Error", "Please enter a project description"); setIsSaving(false); return; } const url = projectId ? `/api/projects/${projectId}` : "/api/projects"; const method = projectId ? "PUT" : "POST"; // Prepare data for saving - only include fields that exist in the database schema const saveTitle = editLocale === "en" ? formData.title.trim() : (baseTexts?.title || formData.title.trim()); const saveDescription = editLocale === "en" ? formData.description.trim() : (baseTexts?.description || formData.description.trim()); const saveData = { title: saveTitle, description: saveDescription, content: formData.content.trim(), category: formData.category, tags: formData.tags, github: formData.github.trim() || null, live: formData.live.trim() || null, imageUrl: formData.image.trim() || null, published: formData.published, featured: formData.featured, // Add required fields that might be missing date: new Date().toISOString().split("T")[0], // Current date in YYYY-MM-DD format }; const response = await fetch(url, { method, headers: { "Content-Type": "application/json", "x-admin-request": "true", "x-session-token": sessionStorage.getItem("admin_session_token") || "", }, body: JSON.stringify(saveData), }); if (response.ok) { const savedProject = await response.json(); // Update local state with the saved project data setProject(savedProject); setFormData((prev) => ({ ...prev, title: savedProject.title || "", description: savedProject.description || "", content: savedProject.content || "", category: savedProject.category || "web", tags: savedProject.tags || [], featured: savedProject.featured || false, published: savedProject.published || false, github: savedProject.github || "", live: savedProject.live || "", image: savedProject.imageUrl || "", })); // Show success toast (smaller, smoother) showSuccess("Saved", `"${savedProject.title}" saved`); // Save translation if editing a non-default locale if (projectId && editLocale !== "en") { try { await fetch(`/api/projects/${projectId}/translation`, { method: "PUT", headers: { "Content-Type": "application/json", "x-admin-request": "true", "x-session-token": sessionStorage.getItem("admin_session_token") || "", }, body: JSON.stringify({ locale: editLocale, title: formData.title.trim(), description: formData.description.trim(), }), }); } catch { // ignore translation save failures } } // Update project ID if it was a new project if (!projectId && savedProject.id) { const newUrl = `/editor?id=${savedProject.id}`; window.history.replaceState({}, '', newUrl); } } else { const errorData = await response.json(); if (process.env.NODE_ENV === "development") { console.error("Error saving project:", response.status, errorData); } showError("Save Failed", errorData.error || "Failed to save"); } } catch (error) { if (process.env.NODE_ENV === "development") { console.error("Error saving project:", error); } showError( "Save Failed", error instanceof Error ? error.message : "Failed to save" ); } finally { setIsSaving(false); } }, [projectId, formData, showSuccess, showError, editLocale, baseTexts]); const handleInputChange = ( field: string, value: string | boolean | string[], ) => { setFormData((prev) => { const newData = { ...prev, [field]: value, }; // Add to history for undo/redo setHistory((hist) => { const newHistory = hist.slice(0, historyIndex + 1); newHistory.push(newData); // Keep only last 50 history entries const trimmedHistory = newHistory.slice(-50); setHistoryIndex(trimmedHistory.length - 1); return trimmedHistory; }); return newData; }); }; const handleUndo = useCallback(() => { setHistoryIndex((currentIndex) => { if (currentIndex > 0) { const newIndex = currentIndex - 1; shouldUpdateContentRef.current = true; setFormData(history[newIndex]); return newIndex; } return currentIndex; }); }, [history]); const handleRedo = useCallback(() => { setHistoryIndex((currentIndex) => { if (currentIndex < history.length - 1) { const newIndex = currentIndex + 1; shouldUpdateContentRef.current = true; setFormData(history[newIndex]); return newIndex; } return currentIndex; }); }, [history]); const handleRevert = useCallback(() => { if (originalFormData) { if (confirm("Are you sure you want to revert all changes? This cannot be undone.")) { shouldUpdateContentRef.current = true; setFormData(originalFormData); setHistory([originalFormData]); setHistoryIndex(0); } } else if (projectId) { // Reload from server if (confirm("Are you sure you want to revert all changes? This will reload the project from the server.")) { shouldUpdateContentRef.current = true; loadProject(projectId); } } }, [originalFormData, projectId, loadProject]); // Sync contentEditable when formData.content changes externally (undo/redo/revert) useEffect(() => { if (contentRef.current && shouldUpdateContentRef.current) { const currentContent = contentRef.current.textContent || ""; if (currentContent !== formData.content) { contentRef.current.textContent = formData.content; } shouldUpdateContentRef.current = false; } }, [formData.content]); // Initialize contentEditable when formData.content is set and editor is empty useEffect(() => { if (contentRef.current) { const currentText = contentRef.current.textContent || ""; // Initialize if editor is empty and we have content, or if content changed externally if ((!currentText && formData.content) || (shouldUpdateContentRef.current && currentText !== formData.content)) { contentRef.current.textContent = formData.content; shouldUpdateContentRef.current = false; } } }, [formData.content]); // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Ctrl+S or Cmd+S - Save if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); if (!isSaving) { handleSave(); } } // Ctrl+Z or Cmd+Z - Undo if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); handleUndo(); } // Ctrl+Shift+Z or Cmd+Shift+Z - Redo if ((e.ctrlKey || e.metaKey) && e.key === 'z' && e.shiftKey) { e.preventDefault(); handleRedo(); } // Ctrl+R or Cmd+R - Revert (but allow browser refresh if not in editor) if ((e.ctrlKey || e.metaKey) && e.key === 'r') { const target = e.target as HTMLElement; if (target.isContentEditable || target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { e.preventDefault(); handleRevert(); } } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [isSaving, handleSave, handleUndo, handleRedo, handleRevert]); const handleTagsChange = (tagsString: string) => { const tags = tagsString .split(",") .map((tag) => tag.trim()) .filter((tag) => tag); setFormData((prev) => ({ ...prev, tags, })); }; // Markdown components for react-markdown with security // eslint-disable-next-line @typescript-eslint/no-explicit-any const markdownComponents: any = { a: ({ node: _node, ...props }: { node?: unknown; href?: string; children?: React.ReactNode }) => { // Validate URLs to prevent javascript: and data: protocols const href = props.href || ""; const isSafe = href && typeof href === 'string' && !href.startsWith("javascript:") && !href.startsWith("data:"); return ( ); }, img: ({ node: _node, ...props }: { node?: unknown; src?: string; alt?: string }) => { // Validate image URLs const src = props.src; const isSafe = src && typeof src === 'string' && !src.startsWith("javascript:") && !src.startsWith("data:"); // eslint-disable-next-line @next/next/no-img-element return isSafe ? {props.alt : null; }, }; // Rich text editor functions const insertFormatting = (format: string) => { const content = contentRef.current; if (!content) return; const selection = window.getSelection(); if (!selection || selection.rangeCount === 0) return; const range = selection.getRangeAt(0); let newText = ""; switch (format) { case "bold": newText = `**${selection.toString() || "bold text"}**`; break; case "italic": newText = `*${selection.toString() || "italic text"}*`; break; case "code": newText = `\`${selection.toString() || "code"}\``; break; case "h1": newText = `# ${selection.toString() || "Heading 1"}`; break; case "h2": newText = `## ${selection.toString() || "Heading 2"}`; break; case "h3": newText = `### ${selection.toString() || "Heading 3"}`; break; case "list": newText = `- ${selection.toString() || "List item"}`; break; case "orderedList": newText = `1. ${selection.toString() || "List item"}`; break; case "quote": newText = `> ${selection.toString() || "Quote"}`; break; case "link": const url = prompt("Enter URL:"); if (url) { newText = `[${selection.toString() || "link text"}](${url})`; } break; case "image": const imageUrl = prompt("Enter image URL:"); if (imageUrl) { newText = `![${selection.toString() || "alt text"}](${imageUrl})`; } break; } if (newText) { range.deleteContents(); range.insertNode(document.createTextNode(newText)); // Update form data setFormData((prev) => ({ ...prev, content: content.textContent || "", })); } }; if (isLoading) { return (

Loading Editor

Preparing your workspace...

); } if (!isAuthenticated) { return (

Access Denied

You need to be logged in to access the editor.

); } return (
{/* Header */}

{isCreating ? "Create New Project" : `Edit: ${formData.title || "Untitled"}`}

{/* Editor Content - Scrollable */}
{/* Floating particles background */}
{[...Array(20)].map((_, i) => (
))}
{/* Sidebar - Left (appears first in DOM for left positioning) */}
{/* Project Settings */}

Settings

{editLocale !== "en" && (

Title/description are saved as a translation. Other fields are global.

)}
handleTagsChange(e.target.value)} className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none" placeholder="React, TypeScript, Next.js" />
{/* Links */}

Links

handleInputChange("github", e.target.value) } className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none" placeholder="https://github.com/username/repo" />
handleInputChange("live", e.target.value)} className="w-full px-3 py-2 form-input-enhanced rounded-lg focus:outline-none" placeholder="https://example.com" />
{/* Publish */}

Publish

Preview

Status: {formData.published ? "Published" : "Draft"}

{formData.featured && (

⭐ Featured

)}

Category: {formData.category}

Tags: {formData.tags.length} tags

{/* Main Editor - Right (appears second in DOM for right positioning) */}
{/* Project Title */} handleInputChange("title", e.target.value)} className="w-full text-3xl font-bold form-input-enhanced focus:outline-none p-4 rounded-lg" placeholder="Enter project title..." /> {/* Description - Under Title */}

Description