🔧 Enhance Middleware and Admin Features

 Updated Middleware Logic:
- Enhanced admin route protection with Basic Auth for legacy routes and session-based auth for `/manage` and `/editor`.

 Improved Admin Panel Styles:
- Added glassmorphism styles for admin components to enhance UI aesthetics.

 Refined Rate Limiting:
- Adjusted rate limits for admin dashboard requests to allow more generous access.

 Introduced Analytics Reset API:
- Added a new endpoint for resetting analytics data with rate limiting and admin authentication.

🎯 Overall Improvements:
- Strengthened security and user experience for admin functionalities.
- Enhanced visual design for better usability.
- Streamlined analytics management processes.
This commit is contained in:
2025-09-09 19:50:52 +02:00
parent 0ae1883cf4
commit be01ee2adb
26 changed files with 4518 additions and 1103 deletions

749
components/GhostEditor.tsx Normal file
View File

@@ -0,0 +1,749 @@
'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<GhostEditorProps> = ({
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<string[]>([]);
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<HTMLTextAreaElement>(null);
const contentRef = useRef<HTMLTextAreaElement>(null);
const previewRef = useRef<HTMLDivElement>(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 = `<u>${selection || 'underlined text'}</u>`;
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 = `![${selection || 'alt text'}](image-url)`;
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, '<h3 class="text-xl font-semibold text-white mb-3 mt-6">$1</h3>')
.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-bold text-white mb-4 mt-8">$1</h2>')
.replace(/^# (.*$)/gim, '<h1 class="text-3xl font-bold text-white mb-6 mt-8">$1</h1>')
// Bold and Italic
.replace(/\*\*(.*?)\*\*/g, '<strong class="font-bold">$1</strong>')
.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>')
// Underline and Strikethrough
.replace(/<u>(.*?)<\/u>/g, '<u class="underline">$1</u>')
.replace(/~~(.*?)~~/g, '<del class="line-through opacity-75">$1</del>')
// Code
.replace(/```([^`]+)```/g, '<pre class="bg-gray-800 border border-gray-700 rounded-lg p-4 my-4 overflow-x-auto"><code class="text-green-400 font-mono text-sm">$1</code></pre>')
.replace(/`([^`]+)`/g, '<code class="bg-gray-800 border border-gray-700 rounded px-2 py-1 font-mono text-sm text-green-400">$1</code>')
// Lists
.replace(/^\- (.*$)/gim, '<li class="ml-4 mb-1">• $1</li>')
.replace(/^\d+\. (.*$)/gim, '<li class="ml-4 mb-1 list-decimal">$1</li>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-400 hover:text-blue-300 underline" target="_blank">$1</a>')
// Images
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="max-w-full h-auto rounded-lg my-4" />')
// Quotes
.replace(/^> (.*$)/gim, '<blockquote class="border-l-4 border-blue-500 pl-4 py-2 my-4 bg-gray-800/50 italic text-gray-300">$1</blockquote>')
// Dividers
.replace(/^---$/gim, '<hr class="border-gray-600 my-8" />')
// Paragraphs
.replace(/\n\n/g, '</p><p class="mb-4 text-gray-200 leading-relaxed">')
.replace(/\n/g, '<br />');
return `<div class="prose prose-invert max-w-none"><p class="mb-4 text-gray-200 leading-relaxed">${html}</p></div>`;
};
if (!isOpen) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/95 backdrop-blur-sm z-50"
>
{/* Professional Ghost Editor */}
<div className="h-full flex flex-col bg-gray-900">
{/* Top Navigation Bar */}
<div className="flex items-center justify-between p-4 border-b border-gray-700 bg-gray-800">
<div className="flex items-center space-x-4">
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium text-white">
{isCreating ? 'New Project' : 'Editing Project'}
</span>
</div>
<div className="flex items-center space-x-2">
{published ? (
<span className="px-3 py-1 bg-green-600 text-white rounded-full text-sm font-medium">
Published
</span>
) : (
<span className="px-3 py-1 bg-gray-600 text-gray-300 rounded-full text-sm font-medium">
Draft
</span>
)}
{featured && (
<span className="px-3 py-1 bg-purple-600 text-white rounded-full text-sm font-medium">
Featured
</span>
)}
</div>
</div>
{/* View Mode Toggle */}
<div className="flex items-center space-x-2">
<div className="flex items-center bg-gray-700 rounded-lg p-1">
<button
onClick={() => setViewMode('edit')}
className={`p-2 rounded transition-colors ${
viewMode === 'edit' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
}`}
title="Edit Mode"
>
<Type className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('split')}
className={`p-2 rounded transition-colors ${
viewMode === 'split' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
}`}
title="Split View"
>
<Columns className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('preview')}
className={`p-2 rounded transition-colors ${
viewMode === 'preview' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
}`}
title="Preview Mode"
>
<Eye className="w-4 h-4" />
</button>
</div>
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-2 rounded-lg transition-colors ${
showSettings ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-gray-700'
}`}
>
<Settings className="w-5 h-5" />
</button>
<button
onClick={handleSave}
className="flex items-center space-x-2 px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
>
<Save className="w-4 h-4" />
<span>Save</span>
</button>
</div>
</div>
{/* Rich Text Toolbar */}
<div className="flex items-center justify-between p-3 border-b border-gray-700 bg-gray-800/50">
<div className="flex items-center space-x-1">
{/* Text Formatting */}
<div className="flex items-center space-x-1 pr-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('bold')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Bold (Ctrl+B)"
>
<Bold className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('italic')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Italic (Ctrl+I)"
>
<Italic className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('underline')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Underline"
>
<Underline className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('strikethrough')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Strikethrough"
>
<Strikethrough className="w-4 h-4" />
</button>
</div>
{/* Headers */}
<div className="flex items-center space-x-1 px-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('heading1')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 1"
>
H1
</button>
<button
onClick={() => insertMarkdown('heading2')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 2"
>
H2
</button>
<button
onClick={() => insertMarkdown('heading3')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 3"
>
H3
</button>
</div>
{/* Lists */}
<div className="flex items-center space-x-1 px-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('list')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Bullet List"
>
<List className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('list-ordered')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Numbered List"
>
<ListOrdered className="w-4 h-4" />
</button>
</div>
{/* Insert Elements */}
<div className="flex items-center space-x-1 px-2">
<button
onClick={() => insertMarkdown('link')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Insert Link"
>
<Link2 className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('image')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Insert Image"
>
<ImageIcon className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('code')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Code Block"
>
<Code className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('quote')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Quote"
>
<Quote className="w-4 h-4" />
</button>
</div>
</div>
{/* Stats */}
<div className="flex items-center space-x-4 text-sm text-gray-400">
<span>{wordCount} words</span>
<span>{readingTime} min read</span>
</div>
</div>
{/* Main Editor Area */}
<div className="flex-1 flex overflow-hidden">
{/* Content Area */}
<div className="flex-1 flex">
{/* Editor Pane */}
{(viewMode === 'edit' || viewMode === 'split') && (
<div className={`${viewMode === 'split' ? 'w-1/2' : 'w-full'} flex flex-col bg-gray-900`}>
{/* Title & Description */}
<div className="p-8 border-b border-gray-800">
<textarea
ref={titleRef}
value={title}
onChange={(e) => {
setTitle(e.target.value);
autoResizeTextarea(e.target);
}}
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
placeholder="Project title..."
className="w-full text-5xl font-bold text-white bg-transparent border-none outline-none placeholder-gray-500 resize-none overflow-hidden leading-tight mb-6"
rows={1}
/>
<textarea
value={description}
onChange={(e) => {
setDescription(e.target.value);
autoResizeTextarea(e.target);
}}
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
placeholder="Brief description of your project..."
className="w-full text-xl text-gray-300 bg-transparent border-none outline-none placeholder-gray-500 resize-none overflow-hidden leading-relaxed"
rows={1}
/>
</div>
{/* Content Editor */}
<div className="flex-1 p-8">
<textarea
ref={contentRef}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Start writing your story...
Use Markdown for formatting:
**Bold text** or *italic text*
# Large heading
## Medium heading
### Small heading
- Bullet points
1. Numbered lists
> Quotes
`code`
[Links](https://example.com)
![Images](image-url)"
className="w-full h-full text-lg text-white bg-transparent border-none outline-none placeholder-gray-600 resize-none font-mono leading-relaxed focus:ring-0"
style={{ minHeight: '500px' }}
/>
</div>
</div>
)}
{/* Preview Pane */}
{(viewMode === 'preview' || viewMode === 'split') && (
<div className={`${viewMode === 'split' ? 'w-1/2 border-l border-gray-700' : 'w-full'} bg-gray-850 overflow-y-auto`}>
<div className="p-8">
{/* Preview Header */}
<div className="mb-8 border-b border-gray-700 pb-8">
<h1 className="text-5xl font-bold text-white mb-6 leading-tight">
{title || 'Project title...'}
</h1>
<p className="text-xl text-gray-300 leading-relaxed">
{description || 'Brief description of your project...'}
</p>
</div>
{/* Preview Content */}
<div
ref={previewRef}
className="prose prose-invert max-w-none"
dangerouslySetInnerHTML={{
__html: content ? renderMarkdownPreview(content) : '<p class="text-gray-500 italic">Start writing to see the preview...</p>'
}}
/>
</div>
</div>
)}
</div>
{/* Settings Sidebar */}
<AnimatePresence>
{showSettings && (
<motion.div
initial={{ x: 320 }}
animate={{ x: 0 }}
exit={{ x: 320 }}
className="w-80 bg-gray-800 border-l border-gray-700 flex flex-col"
>
<div className="p-6 border-b border-gray-700">
<h3 className="text-lg font-semibold text-white">Project Settings</h3>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-8">
{/* Status */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Publication</h4>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-white">Published</span>
<button
onClick={() => setPublished(!published)}
className={`w-12 h-6 rounded-full transition-colors relative ${
published ? 'bg-green-600' : 'bg-gray-600'
}`}
>
<div className={`w-4 h-4 bg-white rounded-full transition-transform absolute top-1 ${
published ? 'translate-x-7' : 'translate-x-1'
}`} />
</button>
</div>
<div className="flex items-center justify-between">
<span className="text-white">Featured</span>
<button
onClick={() => setFeatured(!featured)}
className={`w-12 h-6 rounded-full transition-colors relative ${
featured ? 'bg-purple-600' : 'bg-gray-600'
}`}
>
<div className={`w-4 h-4 bg-white rounded-full transition-transform absolute top-1 ${
featured ? 'translate-x-7' : 'translate-x-1'
}`} />
</button>
</div>
</div>
</div>
{/* Category & Difficulty */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Classification</h4>
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">Category</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<div>
<label className="block text-gray-300 text-sm mb-2">Difficulty</label>
<select
value={difficulty}
onChange={(e) => setDifficulty(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{difficulties.map(diff => (
<option key={diff} value={diff}>{diff}</option>
))}
</select>
</div>
</div>
</div>
{/* Links */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">External Links</h4>
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">
<Github className="w-4 h-4 inline mr-1" />
GitHub Repository
</label>
<input
type="url"
value={github}
onChange={(e) => setGithub(e.target.value)}
placeholder="https://github.com/..."
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"
/>
</div>
<div>
<label className="block text-gray-300 text-sm mb-2">
<Globe className="w-4 h-4 inline mr-1" />
Live Demo
</label>
<input
type="url"
value={live}
onChange={(e) => setLive(e.target.value)}
placeholder="https://..."
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"
/>
</div>
</div>
</div>
{/* Tags */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Tags</h4>
<div className="space-y-3">
<input
type="text"
placeholder="Add a tag and press Enter"
onKeyDown={(e) => {
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 && (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center space-x-1 px-3 py-1 bg-blue-600 text-white rounded-full text-sm"
>
<span>{tag}</span>
<button
onClick={() => removeTag(tag)}
className="text-blue-200 hover:text-white"
>
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</motion.div>
</AnimatePresence>
);
};