This commit is contained in:
Dennis Konkol
2025-09-02 23:46:36 +00:00
parent ded873e6b4
commit 203a332306
22 changed files with 3886 additions and 194 deletions

View File

@@ -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
![App Screenshot](screenshot-url)
## 🔗 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
![Mobile Screenshot](screenshot-url)
## 🔗 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>