Files
portfolio/app/admin/page.tsx
Dennis Konkol ded873e6b4 🎨 Complete Portfolio Redesign: Modern Dark Theme + Admin Dashboard + Enhanced Markdown Editor
 New Features:
- Complete dark theme redesign with glassmorphism effects
- Responsive admin dashboard with collapsible projects list
- Enhanced markdown editor with live preview
- Project image upload functionality
- Improved project management (create, edit, delete, publish/unpublish)
- Slug-based project URLs
- Legal pages (Impressum, Privacy Policy)
- Modern animations with Framer Motion

🔧 Improvements:
- Fixed hydration errors with mounted state
- Enhanced UI/UX with better spacing and proportions
- Improved markdown rendering with custom components
- Better project image placeholders with initials
- Conditional rendering for GitHub/Live Demo links
- Enhanced toolbar with categorized quick actions
- Responsive grid layout for admin dashboard

📱 Technical:
- Next.js 15 + TypeScript + Tailwind CSS
- Local storage for project persistence
- Optimized performance and responsive design
2025-09-01 23:30:10 +00:00

859 lines
40 KiB
TypeScript

"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
Save,
Eye,
Plus,
Edit,
Trash2,
Upload,
Bold,
Italic,
List,
Link as LinkIcon,
Image as ImageIcon,
Code,
Quote,
ArrowLeft,
ChevronDown,
ChevronRight,
Palette,
Smile,
FileText,
Download,
Upload as UploadIcon,
Settings,
Smartphone
} from 'lucide-react';
import Link from 'next/link';
import ReactMarkdown from 'react-markdown';
interface Project {
id: number;
title: string;
description: string;
content: string;
tags: string[];
featured: boolean;
category: string;
date: string;
github?: string;
live?: string;
published: boolean;
imageUrl?: string;
metaDescription?: string;
keywords?: string;
ogImage?: string;
schema?: any;
}
const AdminPage = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const [projects, setProjects] = useState<Project[]>([]);
// Load projects from localStorage on mount
useEffect(() => {
const savedProjects = localStorage.getItem('portfolio-projects');
if (savedProjects) {
setProjects(JSON.parse(savedProjects));
} else {
// Default projects if none exist
const defaultProjects: Project[] = [
{
id: 1,
title: "Portfolio Website",
description: "A modern, responsive portfolio website built with Next.js, TypeScript, and Tailwind CSS.",
content: "# Portfolio Website\n\nThis is my personal portfolio website built with modern web technologies. The site features a dark theme with glassmorphism effects and smooth animations.\n\n## Features\n\n- **Responsive Design**: Works perfectly on all devices\n- **Dark Theme**: Modern dark mode with glassmorphism effects\n- **Animations**: Smooth animations powered by Framer Motion\n- **Markdown Support**: Projects are written in Markdown for easy editing\n- **Performance**: Optimized for speed and SEO\n\n## Technologies Used\n\n- Next.js 15\n- TypeScript\n- Tailwind CSS\n- Framer Motion\n- React Markdown\n\n## Development Process\n\nThe website was designed with a focus on user experience and performance. I used modern CSS techniques like CSS Grid, Flexbox, and custom properties to create a responsive layout.\n\n## Future Improvements\n\n- Add blog functionality\n- Implement project filtering\n- Add more interactive elements\n- Optimize for Core Web Vitals\n\n## Links\n\n- [Live Demo](https://dki.one)\n- [GitHub Repository](https://github.com/Denshooter/portfolio)",
tags: ["Next.js", "TypeScript", "Tailwind CSS", "Framer Motion"],
featured: true,
category: "Web Development",
date: "2024"
}
];
setProjects(defaultProjects);
localStorage.setItem('portfolio-projects', JSON.stringify(defaultProjects));
}
}, []);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [isPreview, setIsPreview] = useState(false);
const [isProjectsCollapsed, setIsProjectsCollapsed] = useState(false);
const [formData, setFormData] = useState({
title: '',
description: '',
content: '',
tags: '',
category: '',
featured: false,
github: '',
live: '',
published: true,
imageUrl: ''
});
const [markdownContent, setMarkdownContent] = useState('');
const categories = [
"Web Development",
"Full-Stack",
"Web Application",
"Mobile App",
"Desktop App",
"API Development",
"Database Design",
"DevOps",
"UI/UX Design",
"Game Development",
"Machine Learning",
"Data Science",
"Blockchain",
"IoT",
"Cybersecurity"
];
if (!mounted) {
return null;
}
const handleSave = () => {
if (!formData.title || !formData.description || !markdownContent || !formData.category) {
alert('Please fill in all required fields!');
return;
}
try {
if (selectedProject) {
// Update existing project
const updatedProjects = projects.map(p =>
p.id === selectedProject.id
? {
...p,
title: formData.title,
description: formData.description,
content: markdownContent,
tags: formData.tags ? formData.tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [],
category: formData.category,
featured: formData.featured,
github: formData.github || undefined,
live: formData.live || undefined,
published: formData.published,
imageUrl: formData.imageUrl || undefined
}
: p
);
setProjects(updatedProjects);
localStorage.setItem('portfolio-projects', JSON.stringify(updatedProjects));
console.log('Project updated successfully:', selectedProject.id);
} else {
// Create new project
const newProject: Project = {
id: Math.floor(Math.random() * 1000000),
title: formData.title,
description: formData.description,
content: markdownContent,
tags: formData.tags ? formData.tags.split(',').map(tag => tag.trim()).filter(tag => tag) : [],
category: formData.category,
featured: formData.featured,
github: formData.github || undefined,
live: formData.live || undefined,
published: formData.published,
imageUrl: formData.imageUrl || undefined,
date: new Date().getFullYear().toString()
};
const updatedProjects = [...projects, newProject];
setProjects(updatedProjects);
localStorage.setItem('portfolio-projects', JSON.stringify(updatedProjects));
console.log('New project created successfully:', newProject.id);
}
resetForm();
alert('Project saved successfully!');
} catch (error) {
console.error('Error saving project:', error);
alert('Error saving project. Please try again.');
}
};
const handleEdit = (project: Project) => {
console.log('Editing project:', project);
setSelectedProject(project);
setFormData({
title: project.title,
description: project.description,
content: project.content,
tags: project.tags.join(', '),
category: project.category,
featured: project.featured,
github: project.github || '',
live: project.live || '',
published: project.published !== undefined ? project.published : true,
imageUrl: project.imageUrl || ''
});
setMarkdownContent(project.content);
setIsPreview(false);
};
const handleDelete = (projectId: number) => {
if (confirm('Are you sure you want to delete this project?')) {
const updatedProjects = projects.filter(p => p.id !== projectId);
setProjects(updatedProjects);
localStorage.setItem('portfolio-projects', JSON.stringify(updatedProjects));
}
};
const resetForm = () => {
console.log('Resetting form');
setSelectedProject(null);
setFormData({
title: '',
description: '',
content: '',
tags: '',
category: '',
featured: false,
github: '',
live: '',
published: true,
imageUrl: ''
});
setMarkdownContent('');
setIsPreview(false);
};
const insertMarkdown = (type: string) => {
const textarea = document.getElementById('markdown-editor') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = textarea.value;
let insertion = '';
let cursorOffset = 0;
switch (type) {
case 'h1':
insertion = `# ${text.substring(start, end) || 'Heading'}`;
cursorOffset = 2;
break;
case 'h2':
insertion = `## ${text.substring(start, end) || 'Heading'}`;
cursorOffset = 3;
break;
case 'bold':
insertion = `**${text.substring(start, end) || 'bold text'}**`;
cursorOffset = 2;
break;
case 'italic':
insertion = `*${text.substring(start, end) || 'italic text'}*`;
cursorOffset = 1;
break;
case 'list':
insertion = `- ${text.substring(start, end) || 'list item'}`;
cursorOffset = 2;
break;
case 'link':
insertion = `[${text.substring(start, end) || 'link text'}](url)`;
cursorOffset = 3;
break;
case 'image':
insertion = `![alt text](image-url)`;
cursorOffset = 9;
break;
case 'code':
insertion = `\`${text.substring(start, end) || 'code'}\``;
cursorOffset = 1;
break;
case 'quote':
insertion = `> ${text.substring(start, end) || 'quote text'}`;
cursorOffset = 2;
break;
case 'table':
insertion = `| Header 1 | Header 2 | Header 3 |\n|----------|----------|----------|\n| Cell 1 | Cell 2 | Cell 3 |\n| Cell 4 | Cell 5 | Cell 6 |`;
cursorOffset = 0;
break;
}
const newText = text.substring(0, start) + insertion + text.substring(end);
setMarkdownContent(newText);
// Set cursor position and select the placeholder text for easy editing
setTimeout(() => {
textarea.focus();
if (type === 'h1' || type === 'h2') {
// For headings, select the placeholder text so user can type directly
const placeholderStart = start + (type === 'h1' ? 2 : 3);
const placeholderEnd = start + insertion.length;
textarea.setSelectionRange(placeholderStart, placeholderEnd);
} else {
// For other elements, position cursor appropriately
textarea.setSelectionRange(start + insertion.length - cursorOffset, start + insertion.length - cursorOffset);
}
}, 0);
};
const handleProjectImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const file = files[0];
if (file) {
// Simulate image upload - in production you'd upload to a real service
const imageUrl = URL.createObjectURL(file);
setFormData(prev => ({ ...prev, imageUrl }));
}
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const file = files[0];
if (file) {
// Create a more descriptive image URL for better organization
const imageName = file.name.replace(/\.[^/.]+$/, ""); // Remove file extension
const imageUrl = URL.createObjectURL(file);
const textarea = document.getElementById('markdown-editor') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const text = textarea.value;
// Insert image with better alt text and a newline for spacing
const insertion = `\n![${imageName}](${imageUrl})\n`;
const newText = text.substring(0, start) + insertion + text.substring(start);
setMarkdownContent(newText);
// Focus back to textarea and position cursor after the image
setTimeout(() => {
textarea.focus();
const newCursorPos = start + insertion.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
}, 0);
}
};
return (
<div className="min-h-screen animated-bg">
<div className="max-w-7xl mx-auto px-4 py-20">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="mb-12"
>
<Link
href="/"
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors mb-6"
>
<ArrowLeft size={20} />
<span>Back to Home</span>
</Link>
<h1 className="text-5xl md:text-6xl font-bold mb-6 gradient-text">
Admin Dashboard
</h1>
<p className="text-xl text-gray-400 max-w-3xl">
Manage your projects with the built-in Markdown editor. Create, edit, and preview your content easily.
</p>
</motion.div>
{/* Projects Toggle Button - Always Visible */}
<div className="flex justify-center mb-6">
<motion.button
onClick={() => setIsProjectsCollapsed(!isProjectsCollapsed)}
className="flex items-center space-x-2 px-6 py-3 bg-gradient-to-r from-gray-700 to-gray-800 hover:from-gray-600 hover:to-gray-700 rounded-xl text-white transition-all duration-200 hover:scale-105 border border-gray-600/50 shadow-lg"
title={isProjectsCollapsed ? "Show Projects" : "Hide Projects"}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{isProjectsCollapsed ? (
<>
<ChevronRight size={20} />
<span>Show Projects</span>
</>
) : (
<>
<ChevronDown size={20} />
<span>Hide Projects</span>
</>
)}
</motion.button>
</div>
<div className={`grid gap-8 ${isProjectsCollapsed ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-3'}`}>
{/* Projects List */}
<div className={`${isProjectsCollapsed ? 'hidden' : 'lg:col-span-1'}`}>
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="glass-card p-6 rounded-2xl"
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">Projects</h2>
<button
onClick={resetForm}
className="p-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white transition-colors"
>
<Plus size={20} />
</button>
</div>
<div className="space-y-3">
{projects.map((project) => (
<div
key={project.id}
className={`p-3 rounded-lg cursor-pointer transition-all ${
selectedProject?.id === project.id
? 'bg-blue-600/20 border border-blue-500/50'
: 'bg-gray-800/30 hover:bg-gray-700/30'
}`}
onClick={() => handleEdit(project)}
>
<h3 className="font-medium text-white mb-1">{project.title}</h3>
<p className="text-sm text-gray-400">{project.description}</p>
<div className="flex items-center justify-between mt-2">
<span className="text-xs text-gray-500">{project.category}</span>
<div className="flex space-x-2">
<button
onClick={(e) => {
e.stopPropagation();
handleEdit(project);
}}
className="p-1 text-gray-400 hover:text-blue-400 transition-colors"
>
<Edit size={16} />
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDelete(project.id);
}}
className="p-1 text-gray-400 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
</button>
</div>
</div>
</div>
))}
</div>
</motion.div>
</div>
{/* Editor */}
<div className={`${isProjectsCollapsed ? 'lg:col-span-1' : 'lg:col-span-2'}`}>
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
className="glass-card p-6 rounded-2xl"
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-2xl font-bold text-white">
{selectedProject ? 'Edit Project' : 'New Project'}
</h2>
<div className="flex space-x-3">
<button
onClick={() => setIsPreview(!isPreview)}
className={`px-4 py-2 rounded-lg transition-colors ${
isPreview
? 'bg-blue-600 text-white'
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
}`}
title="Toggle Preview"
>
<Eye size={20} />
</button>
<button
onClick={handleSave}
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors flex items-center space-x-2"
title="Save Project"
>
<Save size={20} />
<span>Save</span>
</button>
{selectedProject && (
<button
onClick={() => {
setSelectedProject(null);
resetForm();
}}
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
title="Cancel Edit"
>
Cancel
</button>
)}
</div>
</div>
{!isPreview ? (
<div className="space-y-6">
{/* Basic Info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Title
</label>
<input
type="text"
value={formData.title}
onChange={(e) => setFormData({...formData, title: e.target.value})}
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Project title"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Category
</label>
<select
value={formData.category}
onChange={(e) => setFormData({...formData, category: e.target.value})}
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="">Select category</option>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
</div>
{/* Links */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
GitHub URL (optional)
</label>
<input
type="url"
value={formData.github || ''}
onChange={(e) => setFormData({...formData, github: e.target.value})}
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="https://github.com/username/repo"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Live Demo URL (optional)
</label>
<input
type="url"
value={formData.live || ''}
onChange={(e) => setFormData({...formData, live: e.target.value})}
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="https://demo.example.com"
/>
</div>
</div>
{/* Project Image */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Project Image (optional)
</label>
<div className="flex items-center space-x-4">
<div className="w-24 h-24 bg-gradient-to-br from-gray-700 to-gray-800 rounded-xl border-2 border-dashed border-gray-600 flex items-center justify-center overflow-hidden">
{formData.imageUrl ? (
<img
src={formData.imageUrl}
alt="Project preview"
className="w-full h-full object-cover rounded-lg"
/>
) : (
<div className="text-center">
<span className="text-2xl font-bold text-white">
{formData.title ? formData.title.split(' ').map(word => word[0]).join('').toUpperCase() : 'P'}
</span>
</div>
)}
</div>
<div className="flex-1">
<input
type="file"
accept="image/*"
onChange={handleProjectImageUpload}
className="hidden"
id="project-image-upload"
/>
<label
htmlFor="project-image-upload"
className="inline-flex items-center px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white cursor-pointer transition-colors"
>
<Upload size={16} className="mr-2" />
Choose Image
</label>
<p className="text-sm text-gray-400 mt-1">Upload a project image or use auto-generated initials</p>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Description
</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({...formData, description: e.target.value})}
rows={3}
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
placeholder="Brief project description"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Tags (comma-separated)
</label>
<input
type="text"
value={formData.tags}
onChange={(e) => setFormData({...formData, tags: e.target.value})}
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Next.js, TypeScript, Tailwind CSS"
/>
</div>
<div className="flex items-center justify-between">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={formData.featured}
onChange={(e) => setFormData({...formData, featured: e.target.checked})}
className="w-4 h-4 text-blue-600 bg-gray-800 border-gray-700 rounded focus:ring-blue-500 focus:ring-2"
/>
<span className="text-sm text-gray-300">Featured Project</span>
</label>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={formData.published}
onChange={(e) => setFormData({...formData, published: e.target.checked})}
className="w-4 h-4 text-green-600 bg-gray-800 border-gray-700 rounded focus:ring-green-500 focus:ring-2"
/>
<span className="text-sm text-gray-300">Published</span>
</label>
</div>
{/* Markdown Editor with Live Preview */}
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Content (Markdown)
</label>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Editor */}
<div className="space-y-4">
{/* Image Upload - Moved to top */}
<div className="bg-gradient-to-r from-gray-800/30 to-gray-700/30 p-4 rounded-xl border border-gray-600/30 mb-4">
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-300">📁 Image Upload</span>
<span className="text-xs text-gray-500">Add images to your content</span>
</div>
<label className="flex items-center justify-center space-x-3 p-4 bg-gray-700/50 hover:bg-gray-600/50 rounded-lg cursor-pointer transition-all duration-200 hover:scale-105 border-2 border-dashed border-gray-600/50 hover:border-blue-500/50">
<Upload size={20} className="text-gray-400" />
<span className="text-gray-300 font-medium">Upload Images</span>
<input
type="file"
accept="image/*"
multiple
onChange={handleImageUpload}
className="hidden"
/>
</label>
<p className="text-xs text-gray-500 text-center mt-2">
Drag & drop images or click to browse Images will be inserted at cursor position
</p>
</div>
{/* Enhanced Toolbar */}
<div className="bg-gradient-to-r from-gray-800/30 to-gray-700/30 p-4 rounded-xl border border-gray-600/30">
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-medium text-gray-300">Quick Actions</span>
<span className="text-xs text-gray-500">Click to insert</span>
</div>
{/* Text Formatting */}
<div className="mb-4">
<div className="text-xs text-gray-500 mb-2 uppercase tracking-wide">Text Formatting</div>
<div className="grid grid-cols-6 gap-2">
<button
onClick={() => insertMarkdown('h1')}
className="p-3 bg-gradient-to-br from-blue-600/50 to-blue-700/50 hover:from-blue-500/60 hover:to-blue-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-blue-500/50 shadow-lg"
title="Heading 1"
>
<span className="text-sm font-bold">H1</span>
</button>
<button
onClick={() => insertMarkdown('h2')}
className="p-3 bg-gradient-to-br from-purple-600/50 to-purple-700/50 hover:from-purple-500/60 hover:to-purple-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-purple-500/50 shadow-lg"
title="Heading 2"
>
<span className="text-sm font-bold">H2</span>
</button>
<button
onClick={() => insertMarkdown('bold')}
className="p-3 bg-gradient-to-br from-gray-700/50 to-gray-800/50 hover:from-gray-600/60 hover:to-gray-700/60 rounded-lg text-gray-300 hover:text-white transition-all duration-200 hover:scale-105 border border-gray-600/50 shadow-lg"
title="Bold"
>
<Bold size={16} />
</button>
<button
onClick={() => insertMarkdown('italic')}
className="p-3 bg-gradient-to-br from-gray-700/50 to-gray-800/50 hover:from-gray-600/60 hover:to-gray-700/60 rounded-lg text-gray-300 hover:text-white transition-all duration-200 hover:scale-105 border border-gray-600/50 shadow-lg"
title="Italic"
>
<Italic size={16} />
</button>
<button
onClick={() => insertMarkdown('code')}
className="p-3 bg-gradient-to-br from-gray-700/50 to-gray-800/50 hover:from-gray-600/60 hover:to-gray-700/60 rounded-lg text-gray-300 hover:text-white transition-all duration-200 hover:scale-105 border border-gray-600/50 shadow-lg"
title="Inline Code"
>
<Code size={16} />
</button>
<button
onClick={() => insertMarkdown('quote')}
className="p-3 bg-gradient-to-br from-gray-700/50 to-gray-800/50 hover:from-gray-600/60 hover:to-gray-700/60 rounded-lg text-gray-300 hover:text-white transition-all duration-200 hover:scale-105 border border-gray-600/50 shadow-lg"
title="Quote"
>
<Quote size={16} />
</button>
</div>
</div>
{/* Content Elements */}
<div className="mb-4">
<div className="text-xs text-gray-500 mb-2 uppercase tracking-wide">Content Elements</div>
<div className="grid grid-cols-4 gap-2">
<button
onClick={() => insertMarkdown('list')}
className="p-3 bg-gradient-to-br from-green-600/50 to-green-700/50 hover:from-green-500/60 hover:to-green-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-green-500/50 shadow-lg"
title="List Item"
>
<List size={16} />
</button>
<button
onClick={() => insertMarkdown('link')}
className="p-3 bg-gradient-to-br from-blue-600/50 to-blue-700/50 hover:from-blue-500/60 hover:to-blue-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-blue-500/50 shadow-lg"
title="Link"
>
<LinkIcon size={16} />
</button>
<button
onClick={() => insertMarkdown('image')}
className="p-3 bg-gradient-to-br from-purple-600/50 to-purple-700/50 hover:from-purple-500/60 hover:to-purple-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-purple-500/50 shadow-lg"
title="Image"
>
<ImageIcon size={16} />
</button>
<button
onClick={() => insertMarkdown('table')}
className="p-3 bg-gradient-to-br from-orange-600/50 to-orange-700/50 hover:from-orange-500/60 hover:to-orange-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-orange-500/50 shadow-lg"
title="Table"
>
<span className="text-sm font-bold">📊</span>
</button>
</div>
</div>
</div>
{/* Enhanced Textarea */}
<div className="relative">
<textarea
id="markdown-editor"
value={markdownContent}
onChange={(e) => setMarkdownContent(e.target.value)}
rows={20}
className="w-full px-6 py-4 bg-gray-800/50 border border-gray-600/50 rounded-xl text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 resize-none font-mono text-sm leading-relaxed shadow-lg"
placeholder="✨ Write your project content in Markdown...&#10;&#10;# Start with a heading&#10;## Add subheadings&#10;- Create lists&#10;- Add **bold** and *italic* text&#10;- Include [links](url) and ![images](url)&#10;- Use `code` and code blocks"
/>
<div className="absolute top-4 right-4 text-xs text-gray-500 font-mono">
{markdownContent.length} chars
</div>
</div>
</div>
{/* Enhanced Live Preview */}
<div className="space-y-4 h-full flex flex-col">
<div className="flex items-center justify-between">
<div className="text-sm font-medium text-gray-300">Live Preview</div>
<div className="flex items-center space-x-2 text-xs text-gray-500">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span>Real-time rendering</span>
</div>
</div>
<div className="flex-1 overflow-y-auto p-6 bg-gradient-to-br from-gray-800/40 to-gray-700/40 rounded-xl border border-gray-600/50 shadow-lg min-h-[32rem]">
<div className="markdown prose prose-invert max-w-none text-white">
{markdownContent ? (
<ReactMarkdown
components={{
h1: ({children}) => <h1 className="text-3xl font-bold text-white mb-4 bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">{children}</h1>,
h2: ({children}) => <h2 className="text-2xl font-semibold text-white mb-3 border-l-4 border-blue-500 pl-3">{children}</h2>,
h3: ({children}) => <h3 className="text-xl font-semibold text-white mb-2">{children}</h3>,
p: ({children}) => <p className="text-gray-300 mb-3 leading-relaxed">{children}</p>,
ul: ({children}) => <ul className="list-disc list-inside text-gray-300 mb-3 space-y-1 marker:text-blue-400">{children}</ul>,
ol: ({children}) => <ol className="list-decimal list-inside text-gray-300 mb-3 space-y-1 marker:text-purple-400">{children}</ol>,
li: ({children}) => <li className="text-gray-300">{children}</li>,
a: ({href, children}) => (
<a href={href} className="text-blue-400 hover:text-blue-300 underline transition-colors decoration-2 underline-offset-2" target="_blank" rel="noopener noreferrer">
{children}
</a>
),
code: ({children}) => <code className="bg-gray-700/80 text-blue-400 px-2 py-1 rounded-md text-sm font-mono border border-gray-600/50">{children}</code>,
pre: ({children}) => <pre className="bg-gray-800/80 p-4 rounded-lg overflow-x-auto mb-3 border border-gray-600/50 shadow-inner">{children}</pre>,
blockquote: ({children}) => <blockquote className="border-l-4 border-blue-500 pl-4 italic text-gray-300 mb-3 bg-blue-500/10 py-2 rounded-r-lg">{children}</blockquote>,
strong: ({children}) => <strong className="font-semibold text-white">{children}</strong>,
em: ({children}) => <em className="italic text-gray-300">{children}</em>
}}
>
{markdownContent}
</ReactMarkdown>
) : (
<div className="text-center text-gray-500 py-20">
<div className="text-6xl mb-4"></div>
<p className="text-lg font-medium">Start writing to see the preview</p>
<p className="text-sm">Your Markdown will appear here in real-time</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
) : (
/* Preview */
<div className="prose prose-invert max-w-none">
<div className="markdown" dangerouslySetInnerHTML={{ __html: markdownContent }} />
</div>
)}
</motion.div>
</div>
</div>
</div>
</div>
);
};
export default AdminPage;