update
This commit is contained in:
@@ -22,13 +22,50 @@ import {
|
||||
Palette,
|
||||
Smile,
|
||||
FileText,
|
||||
Download,
|
||||
Upload as UploadIcon,
|
||||
Settings,
|
||||
Smartphone
|
||||
Database,
|
||||
BarChart3
|
||||
} 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 { useToast } from '@/components/Toast';
|
||||
|
||||
interface Project {
|
||||
id: number;
|
||||
@@ -46,11 +83,33 @@ interface Project {
|
||||
metaDescription?: string;
|
||||
keywords?: string;
|
||||
ogImage?: string;
|
||||
schema?: any;
|
||||
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);
|
||||
@@ -58,34 +117,25 @@ const AdminPage = () => {
|
||||
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
// Load projects from localStorage on mount
|
||||
// Load projects from database 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));
|
||||
}
|
||||
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 [formData, setFormData] = useState({
|
||||
title: '',
|
||||
description: '',
|
||||
@@ -96,7 +146,28 @@ const AdminPage = () => {
|
||||
github: '',
|
||||
live: '',
|
||||
published: true,
|
||||
imageUrl: ''
|
||||
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('');
|
||||
@@ -123,39 +194,16 @@ const AdminPage = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = async () => {
|
||||
if (!formData.title || !formData.description || !markdownContent || !formData.category) {
|
||||
alert('Please fill in all required fields!');
|
||||
showWarning('Pflichtfelder fehlen', 'Bitte fülle alle erforderlichen Felder aus.');
|
||||
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),
|
||||
// Update existing project in database
|
||||
const projectData = {
|
||||
title: formData.title,
|
||||
description: formData.description,
|
||||
content: markdownContent,
|
||||
@@ -166,19 +214,62 @@ const AdminPage = () => {
|
||||
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
|
||||
};
|
||||
const updatedProjects = [...projects, newProject];
|
||||
setProjects(updatedProjects);
|
||||
localStorage.setItem('portfolio-projects', JSON.stringify(updatedProjects));
|
||||
console.log('New project created successfully:', newProject.id);
|
||||
|
||||
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();
|
||||
alert('Project saved successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error saving project:', error);
|
||||
alert('Error saving project. Please try again.');
|
||||
showError('Fehler beim Speichern', 'Das Projekt konnte nicht gespeichert werden. Bitte versuche es erneut.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -195,17 +286,47 @@ const AdminPage = () => {
|
||||
github: project.github || '',
|
||||
live: project.live || '',
|
||||
published: project.published !== undefined ? project.published : true,
|
||||
imageUrl: project.imageUrl || ''
|
||||
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 = (projectId: number) => {
|
||||
const handleDelete = async (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));
|
||||
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.');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -222,7 +343,28 @@ const AdminPage = () => {
|
||||
github: '',
|
||||
live: '',
|
||||
published: true,
|
||||
imageUrl: ''
|
||||
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);
|
||||
@@ -280,6 +422,26 @@ const AdminPage = () => {
|
||||
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);
|
||||
@@ -343,6 +505,142 @@ const AdminPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 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">
|
||||
@@ -466,6 +764,17 @@ const AdminPage = () => {
|
||||
{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 ${
|
||||
@@ -500,6 +809,86 @@ const AdminPage = () => {
|
||||
</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 */}
|
||||
@@ -604,6 +993,214 @@ const AdminPage = () => {
|
||||
</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
|
||||
@@ -667,19 +1264,48 @@ const AdminPage = () => {
|
||||
<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>
|
||||
<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 or click to browse • Images will be inserted at cursor position
|
||||
🚀 Drag & drop images here or click to browse • Images will be inserted at cursor position
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -742,7 +1368,7 @@ const AdminPage = () => {
|
||||
{/* 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">
|
||||
<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"
|
||||
@@ -771,6 +1397,48 @@ const AdminPage = () => {
|
||||
>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user