✨ Features: - Analytics Dashboard with real-time metrics - Redis caching for performance optimization - Import/Export functionality for projects - Complete admin system with security - Production-ready Docker setup 🔧 Technical: - Removed Ghost CMS dependencies - Added Redis container with caching - Implemented API response caching - Enhanced admin interface with analytics - Optimized for dk0.dev domain 🛡️ Security: - Admin authentication with Basic Auth - Protected analytics endpoints - Secure environment configuration 📊 Analytics: - Performance metrics dashboard - Project statistics visualization - Real-time data with caching - Umami integration for GDPR compliance 🎯 Production Ready: - Multi-container Docker setup - Health checks for all services - Automatic restart policies - Resource limits configured - Ready for Nginx Proxy Manager
1578 lines
72 KiB
TypeScript
1578 lines
72 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,
|
||
Settings,
|
||
Database,
|
||
BarChart3,
|
||
TrendingUp
|
||
} from 'lucide-react';
|
||
import Link from 'next/link';
|
||
import ReactMarkdown from 'react-markdown';
|
||
// API functions to replace direct Prisma usage
|
||
const apiService = {
|
||
async getAllProjects() {
|
||
const response = await fetch('/api/projects');
|
||
if (!response.ok) throw new Error('Failed to fetch projects');
|
||
return response.json();
|
||
},
|
||
|
||
async createProject(data: any) {
|
||
const response = await fetch('/api/projects', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (!response.ok) throw new Error('Failed to create project');
|
||
return response.json();
|
||
},
|
||
|
||
async updateProject(id: number, data: any) {
|
||
const response = await fetch(`/api/projects/${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (!response.ok) throw new Error('Failed to update project');
|
||
return response.json();
|
||
},
|
||
|
||
async deleteProject(id: number) {
|
||
const response = await fetch(`/api/projects/${id}`, {
|
||
method: 'DELETE'
|
||
});
|
||
if (!response.ok) throw new Error('Failed to delete project');
|
||
return response.json();
|
||
}
|
||
};
|
||
import AdminDashboard from '@/components/AdminDashboard';
|
||
import ImportExport from '@/components/ImportExport';
|
||
import AnalyticsDashboard from '@/components/AnalyticsDashboard';
|
||
import { useToast } from '@/components/Toast';
|
||
|
||
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?: Record<string, unknown>;
|
||
// New 2.0 features
|
||
difficulty: 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert';
|
||
timeToComplete?: string;
|
||
technologies: string[];
|
||
challenges: string[];
|
||
lessonsLearned: string[];
|
||
futureImprovements: string[];
|
||
demoVideo?: string;
|
||
screenshots: string[];
|
||
colorScheme: string;
|
||
accessibility: boolean;
|
||
performance: {
|
||
lighthouse: number;
|
||
bundleSize: string;
|
||
loadTime: string;
|
||
};
|
||
analytics: {
|
||
views: number;
|
||
likes: number;
|
||
shares: number;
|
||
};
|
||
}
|
||
|
||
const AdminPage = () => {
|
||
const [mounted, setMounted] = useState(false);
|
||
const { showProjectSaved, showProjectDeleted, showError, showWarning } = useToast();
|
||
|
||
useEffect(() => {
|
||
setMounted(true);
|
||
}, []);
|
||
|
||
const [projects, setProjects] = useState<Project[]>([]);
|
||
|
||
// Load projects from database on mount
|
||
useEffect(() => {
|
||
loadProjects();
|
||
}, []);
|
||
|
||
const loadProjects = async () => {
|
||
try {
|
||
const result = await apiService.getAllProjects();
|
||
setProjects(result.projects);
|
||
} catch (error) {
|
||
console.error('Error loading projects from database:', error);
|
||
}
|
||
};
|
||
|
||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||
|
||
const [isPreview, setIsPreview] = useState(false);
|
||
const [isProjectsCollapsed, setIsProjectsCollapsed] = useState(false);
|
||
const [showTemplates, setShowTemplates] = useState(false);
|
||
const [showImportExport, setShowImportExport] = useState(false);
|
||
const [showAnalytics, setShowAnalytics] = useState(false);
|
||
const [formData, setFormData] = useState({
|
||
title: '',
|
||
description: '',
|
||
content: '',
|
||
tags: '',
|
||
category: '',
|
||
featured: false,
|
||
github: '',
|
||
live: '',
|
||
published: true,
|
||
imageUrl: '',
|
||
// New 2.0 fields
|
||
difficulty: 'Intermediate' as 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert',
|
||
timeToComplete: '',
|
||
technologies: '',
|
||
challenges: '',
|
||
lessonsLearned: '',
|
||
futureImprovements: '',
|
||
demoVideo: '',
|
||
screenshots: '',
|
||
colorScheme: 'Dark',
|
||
accessibility: true,
|
||
performance: {
|
||
lighthouse: 90,
|
||
bundleSize: '50KB',
|
||
loadTime: '1.5s'
|
||
},
|
||
analytics: {
|
||
views: 0,
|
||
likes: 0,
|
||
shares: 0
|
||
}
|
||
});
|
||
|
||
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 = async () => {
|
||
if (!formData.title || !formData.description || !markdownContent || !formData.category) {
|
||
showWarning('Pflichtfelder fehlen', 'Bitte fülle alle erforderlichen Felder aus.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (selectedProject) {
|
||
// Update existing project in database
|
||
const projectData = {
|
||
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,
|
||
difficulty: formData.difficulty,
|
||
timeToComplete: formData.timeToComplete || undefined,
|
||
technologies: formData.technologies ? formData.technologies.split(',').map(tech => tech.trim()).filter(tech => tech) : [],
|
||
challenges: formData.challenges ? formData.challenges.split(',').map(challenge => challenge.trim()).filter(challenge => challenge) : [],
|
||
lessonsLearned: formData.lessonsLearned ? formData.lessonsLearned.split(',').map(lesson => lesson.trim()).filter(lesson => lesson) : [],
|
||
futureImprovements: formData.futureImprovements ? formData.futureImprovements.split(',').map(improvement => improvement.trim()).filter(improvement => improvement) : [],
|
||
demoVideo: formData.demoVideo || undefined,
|
||
screenshots: formData.screenshots ? formData.screenshots.split(',').map(screenshot => screenshot.trim()).filter(screenshot => screenshot) : [],
|
||
colorScheme: formData.colorScheme,
|
||
accessibility: formData.accessibility,
|
||
performance: formData.performance,
|
||
analytics: formData.analytics
|
||
};
|
||
|
||
await apiService.updateProject(selectedProject.id, projectData);
|
||
console.log('Project updated successfully in database:', selectedProject.id);
|
||
showProjectSaved(formData.title);
|
||
} else {
|
||
// Create new project in database
|
||
const projectData = {
|
||
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(),
|
||
difficulty: formData.difficulty,
|
||
timeToComplete: formData.timeToComplete || undefined,
|
||
technologies: formData.technologies ? formData.technologies.split(',').map(tech => tech.trim()).filter(tech => tech) : [],
|
||
challenges: formData.challenges ? formData.challenges.split(',').map(challenge => challenge.trim()).filter(challenge => challenge) : [],
|
||
lessonsLearned: formData.lessonsLearned ? formData.lessonsLearned.split(',').map(lesson => lesson.trim()).filter(lesson => lesson) : [],
|
||
futureImprovements: formData.futureImprovements ? formData.futureImprovements.split(',').map(improvement => improvement.trim()).filter(improvement => improvement) : [],
|
||
demoVideo: formData.demoVideo || undefined,
|
||
screenshots: formData.screenshots ? formData.screenshots.split(',').map(screenshot => screenshot.trim()).filter(screenshot => screenshot) : [],
|
||
colorScheme: formData.colorScheme,
|
||
accessibility: formData.accessibility,
|
||
performance: formData.performance,
|
||
analytics: formData.analytics
|
||
};
|
||
|
||
await apiService.createProject(projectData);
|
||
console.log('New project created successfully in database');
|
||
showProjectSaved(formData.title);
|
||
}
|
||
|
||
// Reload projects from database
|
||
await loadProjects();
|
||
resetForm();
|
||
} catch (error) {
|
||
console.error('Error saving project:', error);
|
||
showError('Fehler beim Speichern', 'Das Projekt konnte nicht gespeichert werden. Bitte versuche es erneut.');
|
||
}
|
||
};
|
||
|
||
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 || '',
|
||
// New 2.0 fields
|
||
difficulty: project.difficulty || 'Intermediate' as const,
|
||
timeToComplete: project.timeToComplete || '',
|
||
technologies: project.technologies ? project.technologies.join(', ') : '',
|
||
challenges: project.challenges ? project.challenges.join(', ') : '',
|
||
lessonsLearned: project.lessonsLearned ? project.lessonsLearned.join(', ') : '',
|
||
futureImprovements: project.futureImprovements ? project.futureImprovements.join(', ') : '',
|
||
demoVideo: project.demoVideo || '',
|
||
screenshots: project.screenshots ? project.screenshots.join(', ') : '',
|
||
colorScheme: project.colorScheme || 'Dark',
|
||
accessibility: project.accessibility !== undefined ? project.accessibility : true,
|
||
performance: project.performance || {
|
||
lighthouse: 90,
|
||
bundleSize: '50KB',
|
||
loadTime: '1.5s'
|
||
},
|
||
analytics: project.analytics || {
|
||
views: 0,
|
||
likes: 0,
|
||
shares: 0
|
||
}
|
||
});
|
||
setMarkdownContent(project.content);
|
||
setIsPreview(false);
|
||
};
|
||
|
||
const handleDelete = async (projectId: number) => {
|
||
if (confirm('Are you sure you want to delete this project?')) {
|
||
try {
|
||
const project = projects.find(p => p.id === projectId);
|
||
await apiService.deleteProject(projectId);
|
||
await loadProjects(); // Reload from database
|
||
console.log('Project deleted successfully from database:', projectId);
|
||
if (project) {
|
||
showProjectDeleted(project.title);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting project:', error);
|
||
showError('Fehler beim Löschen', 'Das Projekt konnte nicht gelöscht werden. Bitte versuche es erneut.');
|
||
}
|
||
}
|
||
};
|
||
|
||
const resetForm = () => {
|
||
console.log('Resetting form');
|
||
setSelectedProject(null);
|
||
setFormData({
|
||
title: '',
|
||
description: '',
|
||
content: '',
|
||
tags: '',
|
||
category: '',
|
||
featured: false,
|
||
github: '',
|
||
live: '',
|
||
published: true,
|
||
imageUrl: '',
|
||
// New 2.0 fields
|
||
difficulty: 'Intermediate' as 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert',
|
||
timeToComplete: '',
|
||
technologies: '',
|
||
challenges: '',
|
||
lessonsLearned: '',
|
||
futureImprovements: '',
|
||
demoVideo: '',
|
||
screenshots: '',
|
||
colorScheme: 'Dark',
|
||
accessibility: true,
|
||
performance: {
|
||
lighthouse: 90,
|
||
bundleSize: '50KB',
|
||
loadTime: '1.5s'
|
||
},
|
||
analytics: {
|
||
views: 0,
|
||
likes: 0,
|
||
shares: 0
|
||
}
|
||
});
|
||
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 = ``;
|
||
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;
|
||
case 'emoji':
|
||
insertion = `😊 ${text.substring(start, end) || 'Add your text here'}`;
|
||
cursorOffset = 2;
|
||
break;
|
||
case 'codeblock':
|
||
insertion = `\`\`\`javascript\n${text.substring(start, end) || '// Your code here'}\n\`\`\``;
|
||
cursorOffset = 0;
|
||
break;
|
||
case 'highlight':
|
||
insertion = `==${text.substring(start, end) || 'highlighted text'}==`;
|
||
cursorOffset = 2;
|
||
break;
|
||
case 'spoiler':
|
||
insertion = `||${text.substring(start, end) || 'spoiler text'}||`;
|
||
cursorOffset = 2;
|
||
break;
|
||
case 'callout':
|
||
insertion = `> [!NOTE]\n> ${text.substring(start, end) || 'This is a callout box'}`;
|
||
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\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);
|
||
}
|
||
};
|
||
|
||
// Project Templates
|
||
const projectTemplates = {
|
||
'Web App': {
|
||
title: 'Web Application',
|
||
description: 'A modern web application with responsive design and advanced features.',
|
||
content: `# Web Application
|
||
|
||
## 🚀 Overview
|
||
A modern web application built with cutting-edge technologies.
|
||
|
||
## ✨ Features
|
||
- **Responsive Design**: Works perfectly on all devices
|
||
- **Modern UI/UX**: Beautiful and intuitive user interface
|
||
- **Performance**: Optimized for speed and efficiency
|
||
- **Security**: Built with security best practices
|
||
|
||
## 🛠️ Technologies Used
|
||
- Frontend Framework
|
||
- Backend Technology
|
||
- Database
|
||
- Additional Tools
|
||
|
||
## 📱 Screenshots
|
||

|
||
|
||
## 🔗 Links
|
||
- [Live Demo](demo-url)
|
||
- [GitHub Repository](github-url)
|
||
|
||
## 📈 Future Improvements
|
||
- Feature 1
|
||
- Feature 2
|
||
- Feature 3`,
|
||
difficulty: 'Intermediate' as 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert',
|
||
category: 'Web Application',
|
||
technologies: 'React, Node.js, MongoDB',
|
||
colorScheme: 'Modern and Clean'
|
||
},
|
||
'Mobile App': {
|
||
title: 'Mobile Application',
|
||
description: 'A cross-platform mobile application with native performance.',
|
||
content: `# Mobile Application
|
||
|
||
## 📱 Overview
|
||
A cross-platform mobile application that delivers native performance.
|
||
|
||
## ✨ Features
|
||
- **Cross-Platform**: Works on iOS and Android
|
||
- **Native Performance**: Optimized for mobile devices
|
||
- **Offline Support**: Works without internet connection
|
||
- **Push Notifications**: Keep users engaged
|
||
|
||
## 🛠️ Technologies Used
|
||
- React Native / Flutter
|
||
- Backend API
|
||
- Database
|
||
- Cloud Services
|
||
|
||
## 📸 Screenshots
|
||

|
||
|
||
## 🔗 Links
|
||
- [App Store](app-store-url)
|
||
- [Play Store](play-store-url)
|
||
- [GitHub Repository](github-url)
|
||
|
||
## 📈 Future Improvements
|
||
- Feature 1
|
||
- Feature 2
|
||
- Feature 3`,
|
||
difficulty: 'Advanced' as 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert',
|
||
category: 'Mobile App',
|
||
technologies: 'React Native, Firebase, Node.js',
|
||
colorScheme: 'Mobile-First Design'
|
||
},
|
||
'API Service': {
|
||
title: 'API Service',
|
||
description: 'A robust and scalable API service with comprehensive documentation.',
|
||
content: `# API Service
|
||
|
||
## 🔌 Overview
|
||
A robust and scalable API service that powers multiple applications.
|
||
|
||
## ✨ Features
|
||
- **RESTful Design**: Follows REST API best practices
|
||
- **Authentication**: Secure JWT-based authentication
|
||
- **Rate Limiting**: Prevents abuse and ensures stability
|
||
- **Comprehensive Docs**: Complete API documentation
|
||
- **Testing**: Extensive test coverage
|
||
|
||
## 🛠️ Technologies Used
|
||
- Backend Framework
|
||
- Database
|
||
- Authentication
|
||
- Testing Tools
|
||
|
||
## 📚 API Endpoints
|
||
\`\`\`
|
||
GET /api/users
|
||
POST /api/users
|
||
PUT /api/users/:id
|
||
DELETE /api/users/:id
|
||
\`\`\`
|
||
|
||
## 🔗 Links
|
||
- [API Documentation](docs-url)
|
||
- [GitHub Repository](github-url)
|
||
|
||
## 📈 Future Improvements
|
||
- Feature 1
|
||
- Feature 2
|
||
- Feature 3`,
|
||
difficulty: 'Intermediate' as const,
|
||
category: 'API Development',
|
||
technologies: 'Node.js, Express, MongoDB',
|
||
colorScheme: 'Professional and Clean'
|
||
}
|
||
};
|
||
|
||
const applyTemplate = (templateKey: string) => {
|
||
const template = projectTemplates[templateKey as keyof typeof projectTemplates];
|
||
if (template) {
|
||
setFormData({
|
||
...formData,
|
||
title: template.title,
|
||
description: template.description,
|
||
category: template.category,
|
||
difficulty: template.difficulty,
|
||
technologies: template.technologies,
|
||
colorScheme: template.colorScheme
|
||
});
|
||
setMarkdownContent(template.content);
|
||
setShowTemplates(false);
|
||
}
|
||
};
|
||
|
||
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>
|
||
|
||
{/* Control Buttons */}
|
||
<div className="flex justify-center gap-4 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>
|
||
|
||
<motion.button
|
||
onClick={() => setShowImportExport(!showImportExport)}
|
||
className="flex items-center space-x-2 px-6 py-3 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-500 hover:to-blue-600 rounded-xl text-white transition-all duration-200 hover:scale-105 border border-blue-500/50 shadow-lg"
|
||
title="Import & Export Projects"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<FileText size={20} />
|
||
<span>Import/Export</span>
|
||
</motion.button>
|
||
|
||
<motion.button
|
||
onClick={() => setShowAnalytics(!showAnalytics)}
|
||
className="flex items-center space-x-2 px-6 py-3 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-500 hover:to-green-600 rounded-xl text-white transition-all duration-200 hover:scale-105 border border-green-500/50 shadow-lg"
|
||
title="Analytics Dashboard"
|
||
whileHover={{ scale: 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
>
|
||
<TrendingUp size={20} />
|
||
<span>Analytics</span>
|
||
</motion.button>
|
||
</div>
|
||
|
||
{/* Import/Export Section */}
|
||
{showImportExport && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -20 }}
|
||
className="mb-8"
|
||
>
|
||
<ImportExport />
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* Analytics Section */}
|
||
{showAnalytics && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -20 }}
|
||
className="mb-8"
|
||
>
|
||
<AnalyticsDashboard />
|
||
</motion.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={() => setShowTemplates(!showTemplates)}
|
||
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
showTemplates
|
||
? 'bg-purple-600 text-white'
|
||
: 'bg-gray-700 text-gray-300 hover:bg-gray-600'
|
||
}`}
|
||
title="Project Templates"
|
||
>
|
||
<FileText size={20} />
|
||
</button>
|
||
<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>
|
||
|
||
{/* Project Templates Modal */}
|
||
{showTemplates && (
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.95 }}
|
||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||
onClick={() => setShowTemplates(false)}
|
||
>
|
||
<motion.div
|
||
initial={{ y: 20 }}
|
||
animate={{ y: 0 }}
|
||
className="bg-gray-800 rounded-2xl p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h3 className="text-2xl font-bold text-white">🚀 Project Templates</h3>
|
||
<button
|
||
onClick={() => setShowTemplates(false)}
|
||
className="text-gray-400 hover:text-white transition-colors"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
{Object.entries(projectTemplates).map(([key, template]) => (
|
||
<motion.div
|
||
key={key}
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
className="bg-gray-700/50 rounded-xl p-6 border border-gray-600/50 hover:border-blue-500/50 transition-all cursor-pointer"
|
||
onClick={() => applyTemplate(key)}
|
||
>
|
||
<div className="text-center mb-4">
|
||
<div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full flex items-center justify-center mx-auto mb-3">
|
||
<FileText size={24} className="text-white" />
|
||
</div>
|
||
<h4 className="text-lg font-semibold text-white mb-2">{template.title}</h4>
|
||
<p className="text-sm text-gray-400">{template.description}</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-gray-400">Difficulty:</span>
|
||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||
template.difficulty === 'Beginner' ? 'bg-green-500/20 text-green-400' :
|
||
template.difficulty === 'Intermediate' ? 'bg-yellow-500/20 text-yellow-400' :
|
||
template.difficulty === 'Advanced' ? 'bg-orange-500/20 text-orange-400' :
|
||
'bg-red-500/20 text-red-400'
|
||
}`}>
|
||
{template.difficulty}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-gray-400">Category:</span>
|
||
<span className="text-white">{template.category}</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-gray-400">Tech:</span>
|
||
<span className="text-white text-xs">{template.technologies}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<button className="w-full mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
|
||
Use Template
|
||
</button>
|
||
</motion.div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="mt-6 text-center">
|
||
<p className="text-sm text-gray-400">
|
||
Templates provide a starting point for your projects. Customize them as needed!
|
||
</p>
|
||
</div>
|
||
</motion.div>
|
||
</motion.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>
|
||
|
||
{/* Advanced Project Details - 2.0 Features */}
|
||
<div className="bg-gradient-to-r from-gray-800/20 to-gray-700/20 p-6 rounded-xl border border-gray-600/30">
|
||
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
|
||
<Settings size={20} className="mr-2 text-blue-400" />
|
||
Advanced Project Details
|
||
</h3>
|
||
|
||
{/* Difficulty & Time */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Difficulty Level
|
||
</label>
|
||
<select
|
||
value={formData.difficulty}
|
||
onChange={(e) => setFormData({...formData, difficulty: e.target.value as 'Beginner' | 'Intermediate' | 'Advanced' | 'Expert'})}
|
||
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="Beginner">🟢 Beginner</option>
|
||
<option value="Intermediate">🟡 Intermediate</option>
|
||
<option value="Advanced">🟠 Advanced</option>
|
||
<option value="Expert">🔴 Expert</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Time to Complete
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.timeToComplete}
|
||
onChange={(e) => setFormData({...formData, timeToComplete: 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="e.g., 2-3 weeks, 1 month"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Technologies & Color Scheme */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Technologies (comma-separated)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.technologies}
|
||
onChange={(e) => setFormData({...formData, technologies: 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="React, TypeScript, Node.js"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Color Scheme
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.colorScheme}
|
||
onChange={(e) => setFormData({...formData, colorScheme: 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="e.g., Dark theme, Light theme, Colorful"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Performance Metrics */}
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Lighthouse Score
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min="0"
|
||
max="100"
|
||
value={formData.performance.lighthouse}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
performance: {...formData.performance, lighthouse: parseInt(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"
|
||
placeholder="90"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Bundle Size
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.performance.bundleSize}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
performance: {...formData.performance, bundleSize: 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="45KB"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Load Time
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.performance.loadTime}
|
||
onChange={(e) => setFormData({
|
||
...formData,
|
||
performance: {...formData.performance, loadTime: 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="1.2s"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Learning & Challenges */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Challenges Faced (comma-separated)
|
||
</label>
|
||
<textarea
|
||
value={formData.challenges}
|
||
onChange={(e) => setFormData({...formData, challenges: 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="Performance optimization, Responsive design, State management"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Lessons Learned (comma-separated)
|
||
</label>
|
||
<textarea
|
||
value={formData.lessonsLearned}
|
||
onChange={(e) => setFormData({...formData, lessonsLearned: 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="Advanced CSS techniques, Performance optimization, User experience design"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Future Improvements & Demo */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Future Improvements (comma-separated)
|
||
</label>
|
||
<textarea
|
||
value={formData.futureImprovements}
|
||
onChange={(e) => setFormData({...formData, futureImprovements: 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="AI integration, Advanced analytics, Real-time features"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Demo Video URL (optional)
|
||
</label>
|
||
<input
|
||
type="url"
|
||
value={formData.demoVideo}
|
||
onChange={(e) => setFormData({...formData, demoVideo: 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://youtube.com/watch?v=..."
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Accessibility & Screenshots */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="flex items-center space-x-3">
|
||
<label className="flex items-center space-x-2">
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.accessibility}
|
||
onChange={(e) => setFormData({...formData, accessibility: 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">Accessibility Compliant</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||
Screenshots URLs (comma-separated)
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.screenshots}
|
||
onChange={(e) => setFormData({...formData, screenshots: 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://example.com/screenshot1.jpg, https://example.com/screenshot2.jpg"
|
||
/>
|
||
</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>
|
||
<div
|
||
className="relative"
|
||
onDragOver={(e) => {
|
||
e.preventDefault();
|
||
e.currentTarget.classList.add('border-blue-500', 'bg-blue-500/10');
|
||
}}
|
||
onDragLeave={(e) => {
|
||
e.currentTarget.classList.remove('border-blue-500', 'bg-blue-500/10');
|
||
}}
|
||
onDrop={(e) => {
|
||
e.preventDefault();
|
||
e.currentTarget.classList.remove('border-blue-500', 'bg-blue-500/10');
|
||
const files = Array.from(e.dataTransfer.files);
|
||
files.forEach(file => {
|
||
if (file.type.startsWith('image/')) {
|
||
const imageUrl = URL.createObjectURL(file);
|
||
const textarea = document.getElementById('markdown-editor') as HTMLTextAreaElement;
|
||
if (textarea) {
|
||
const start = textarea.selectionStart;
|
||
const text = textarea.value;
|
||
const insertion = `\n![${file.name.replace(/\.[^/.]+$/, "")}](${imageUrl})\n`;
|
||
const newText = text.substring(0, start) + insertion + text.substring(start);
|
||
setMarkdownContent(newText);
|
||
}
|
||
}
|
||
});
|
||
}}
|
||
>
|
||
<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 group">
|
||
<Upload size={20} className="text-gray-400 group-hover:text-blue-400 transition-colors" />
|
||
<span className="text-gray-300 font-medium group-hover:text-blue-400 transition-colors">Upload Images</span>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={handleImageUpload}
|
||
className="hidden"
|
||
/>
|
||
</label>
|
||
</div>
|
||
<p className="text-xs text-gray-500 text-center mt-2">
|
||
🚀 Drag & drop images here 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-5 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>
|
||
<button
|
||
onClick={() => insertMarkdown('emoji')}
|
||
className="p-3 bg-gradient-to-br from-pink-600/50 to-pink-700/50 hover:from-pink-500/60 hover:to-pink-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-pink-500/50 shadow-lg"
|
||
title="Emoji"
|
||
>
|
||
<Smile size={16} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Advanced Elements */}
|
||
<div className="mb-4">
|
||
<div className="text-xs text-gray-500 mb-2 uppercase tracking-wide">Advanced Elements</div>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
<button
|
||
onClick={() => insertMarkdown('codeblock')}
|
||
className="p-3 bg-gradient-to-br from-indigo-600/50 to-indigo-700/50 hover:from-indigo-500/60 hover:to-indigo-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-indigo-500/50 shadow-lg"
|
||
title="Code Block"
|
||
>
|
||
<FileText size={16} />
|
||
</button>
|
||
<button
|
||
onClick={() => insertMarkdown('highlight')}
|
||
className="p-3 bg-gradient-to-br from-yellow-600/50 to-yellow-700/50 hover:from-yellow-500/60 hover:to-yellow-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-yellow-500/50 shadow-lg"
|
||
title="Highlight Text"
|
||
>
|
||
<Palette size={16} />
|
||
</button>
|
||
<button
|
||
onClick={() => insertMarkdown('spoiler')}
|
||
className="p-3 bg-gradient-to-br from-red-600/50 to-red-700/50 hover:from-red-500/60 hover:to-red-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-red-500/50 shadow-lg"
|
||
title="Spoiler"
|
||
>
|
||
<span className="text-sm font-bold">👁️</span>
|
||
</button>
|
||
<button
|
||
onClick={() => insertMarkdown('callout')}
|
||
className="p-3 bg-gradient-to-br from-teal-600/50 to-teal-700/50 hover:from-teal-500/60 hover:to-teal-600/60 rounded-lg text-white transition-all duration-200 hover:scale-105 border border-teal-500/50 shadow-lg"
|
||
title="Callout Box"
|
||
>
|
||
<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... # Start with a heading ## Add subheadings - Create lists - Add **bold** and *italic* text - Include [links](url) and  - 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;
|