'use client';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Save,
X,
Eye,
EyeOff,
Settings,
Link as LinkIcon,
Tag,
Calendar,
Globe,
Github,
Image as ImageIcon,
Bold,
Italic,
List,
Hash,
Quote,
Code,
Zap,
Type,
Columns,
PanelLeft,
PanelRight,
Monitor,
Smartphone,
Tablet,
Undo,
Redo,
AlignLeft,
AlignCenter,
AlignRight,
Link2,
ListOrdered,
Underline,
Strikethrough
} from 'lucide-react';
interface Project {
id: string;
title: string;
description: string;
content?: string;
category: string;
difficulty?: string;
tags?: string[];
featured: boolean;
published: boolean;
github?: string;
live?: string;
image?: string;
createdAt: string;
updatedAt: string;
}
interface GhostEditorProps {
isOpen: boolean;
onClose: () => void;
project?: Project | null;
onSave: (projectData: any) => void;
isCreating: boolean;
}
export const GhostEditor: React.FC = ({
isOpen,
onClose,
project,
onSave,
isCreating
}) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [content, setContent] = useState('');
const [category, setCategory] = useState('Web Development');
const [tags, setTags] = useState([]);
const [github, setGithub] = useState('');
const [live, setLive] = useState('');
const [featured, setFeatured] = useState(false);
const [published, setPublished] = useState(false);
const [difficulty, setDifficulty] = useState('Intermediate');
// Editor UI state
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'split'>('split');
const [showSettings, setShowSettings] = useState(false);
const [wordCount, setWordCount] = useState(0);
const [readingTime, setReadingTime] = useState(0);
const titleRef = useRef(null);
const contentRef = useRef(null);
const previewRef = useRef(null);
const categories = ['Web Development', 'Full-Stack', 'Web Application', 'Mobile App', 'Design'];
const difficulties = ['Beginner', 'Intermediate', 'Advanced', 'Expert'];
useEffect(() => {
if (project && !isCreating) {
setTitle(project.title);
setDescription(project.description);
setContent(project.content || '');
setCategory(project.category);
setTags(project.tags || []);
setGithub(project.github || '');
setLive(project.live || '');
setFeatured(project.featured);
setPublished(project.published);
setDifficulty(project.difficulty || 'Intermediate');
} else {
// Reset for new project
setTitle('');
setDescription('');
setContent('');
setCategory('Web Development');
setTags([]);
setGithub('');
setLive('');
setFeatured(false);
setPublished(false);
setDifficulty('Intermediate');
}
}, [project, isCreating, isOpen]);
// Calculate word count and reading time
useEffect(() => {
const words = content.trim().split(/\s+/).filter(word => word.length > 0).length;
setWordCount(words);
setReadingTime(Math.ceil(words / 200)); // Average reading speed: 200 words/minute
}, [content]);
const handleSave = () => {
const projectData = {
title,
description,
content,
category,
tags,
github,
live,
featured,
published,
difficulty
};
onSave(projectData);
};
const addTag = (tag: string) => {
if (tag.trim() && !tags.includes(tag.trim())) {
setTags([...tags, tag.trim()]);
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter(tag => tag !== tagToRemove));
};
const insertMarkdown = useCallback((syntax: string, selectedText: string = '') => {
if (!contentRef.current) return;
const textarea = contentRef.current;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = selectedText || content.substring(start, end);
let newText = '';
let cursorOffset = 0;
switch (syntax) {
case 'bold':
newText = `**${selection || 'bold text'}**`;
cursorOffset = selection ? newText.length : 2;
break;
case 'italic':
newText = `*${selection || 'italic text'}*`;
cursorOffset = selection ? newText.length : 1;
break;
case 'underline':
newText = `${selection || 'underlined text'}`;
cursorOffset = selection ? newText.length : 3;
break;
case 'strikethrough':
newText = `~~${selection || 'strikethrough text'}~~`;
cursorOffset = selection ? newText.length : 2;
break;
case 'heading1':
newText = `# ${selection || 'Heading 1'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'heading2':
newText = `## ${selection || 'Heading 2'}`;
cursorOffset = selection ? newText.length : 3;
break;
case 'heading3':
newText = `### ${selection || 'Heading 3'}`;
cursorOffset = selection ? newText.length : 4;
break;
case 'list':
newText = `- ${selection || 'List item'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'list-ordered':
newText = `1. ${selection || 'List item'}`;
cursorOffset = selection ? newText.length : 3;
break;
case 'quote':
newText = `> ${selection || 'Quote'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'code':
if (selection.includes('\n')) {
newText = `\`\`\`\n${selection || 'code block'}\n\`\`\``;
cursorOffset = selection ? newText.length : 4;
} else {
newText = `\`${selection || 'code'}\``;
cursorOffset = selection ? newText.length : 1;
}
break;
case 'link':
newText = `[${selection || 'link text'}](url)`;
cursorOffset = selection ? newText.length - 4 : newText.length - 4;
break;
case 'image':
newText = ``;
cursorOffset = selection ? newText.length - 11 : newText.length - 11;
break;
case 'divider':
newText = '\n---\n';
cursorOffset = newText.length;
break;
default:
return;
}
const newContent = content.substring(0, start) + newText + content.substring(end);
setContent(newContent);
// Focus and set cursor position
setTimeout(() => {
textarea.focus();
const newPosition = start + cursorOffset;
textarea.setSelectionRange(newPosition, newPosition);
}, 0);
}, [content]);
const autoResizeTextarea = (element: HTMLTextAreaElement) => {
element.style.height = 'auto';
element.style.height = element.scrollHeight + 'px';
};
// Render markdown preview
const renderMarkdownPreview = (markdown: string) => {
// Simple markdown renderer for preview
let html = markdown
// Headers
.replace(/^### (.*$)/gim, '$1
')
.replace(/^## (.*$)/gim, '$1
')
.replace(/^# (.*$)/gim, '$1
')
// Bold and Italic
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
// Underline and Strikethrough
.replace(/(.*?)<\/u>/g, '$1')
.replace(/~~(.*?)~~/g, '$1')
// Code
.replace(/```([^`]+)```/g, '$1
')
.replace(/`([^`]+)`/g, '$1')
// Lists
.replace(/^\- (.*$)/gim, '• $1')
.replace(/^\d+\. (.*$)/gim, '$1')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
// Images
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '
')
// Quotes
.replace(/^> (.*$)/gim, '$1
')
// Dividers
.replace(/^---$/gim, '
')
// Paragraphs
.replace(/\n\n/g, '
')
.replace(/\n/g, '
');
return `
`;
};
if (!isOpen) return null;
return (
{/* Professional Ghost Editor */}
{/* Top Navigation Bar */}
{isCreating ? 'New Project' : 'Editing Project'}
{published ? (
Published
) : (
Draft
)}
{featured && (
Featured
)}
{/* View Mode Toggle */}
{/* Rich Text Toolbar */}
{/* Text Formatting */}
{/* Headers */}
{/* Lists */}
{/* Insert Elements */}
{/* Stats */}
{wordCount} words
{readingTime} min read
{/* Main Editor Area */}
{/* Content Area */}
{/* Editor Pane */}
{(viewMode === 'edit' || viewMode === 'split') && (
{/* Title & Description */}
{/* Content Editor */}
)}
{/* Preview Pane */}
{(viewMode === 'preview' || viewMode === 'split') && (
{/* Preview Header */}
{title || 'Project title...'}
{description || 'Brief description of your project...'}
{/* Preview Content */}
Start writing to see the preview...'
}}
/>
)}
{/* Settings Sidebar */}
{showSettings && (
Project Settings
{/* Status */}
Publication
Published
Featured
{/* Category & Difficulty */}
Classification
{/* Links */}
{/* Tags */}
Tags
{
if (e.key === 'Enter') {
e.preventDefault();
addTag(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{tags.length > 0 && (
{tags.map((tag) => (
{tag}
))}
)}
)}
);
};