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>

View File

@@ -4,70 +4,168 @@ import SMTPTransport from "nodemailer/lib/smtp-transport";
import Mail from "nodemailer/lib/mailer";
export async function POST(request: NextRequest) {
const body = (await request.json()) as {
email: string;
name: string;
message: string;
};
const { email, name, message } = body;
try {
const body = (await request.json()) as {
email: string;
name: string;
subject: string;
message: string;
};
const { email, name, subject, message } = body;
const user = process.env.MY_EMAIL ?? "";
const pass = process.env.MY_PASSWORD ?? "";
console.log('📧 Email request received:', { email, name, subject, messageLength: message.length });
if (!user || !pass) {
console.error("Missing email/password environment variables");
return NextResponse.json(
{ error: "Missing EMAIL or PASSWORD" },
{ status: 500 },
);
}
// Validate input
if (!email || !name || !subject || !message) {
console.error('❌ Validation failed: Missing required fields');
return NextResponse.json(
{ error: "Alle Felder sind erforderlich" },
{ status: 400 },
);
}
if (!email || !name || !message) {
console.error("Invalid request body");
return NextResponse.json(
{ error: "Invalid request body" },
{ status: 400 },
);
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
console.error('❌ Validation failed: Invalid email format');
return NextResponse.json(
{ error: "Ungültige E-Mail-Adresse" },
{ status: 400 },
);
}
const transportOptions: SMTPTransport.Options = {
host: "smtp.ionos.de",
port: 587,
secure: false,
requireTLS: true,
auth: {
type: "login",
user,
pass,
},
};
// Validate message length
if (message.length < 10) {
console.error('❌ Validation failed: Message too short');
return NextResponse.json(
{ error: "Nachricht muss mindestens 10 Zeichen lang sein" },
{ status: 400 },
);
}
const transport = nodemailer.createTransport(transportOptions);
const user = process.env.MY_EMAIL ?? "";
const pass = process.env.MY_PASSWORD ?? "";
const mailOptions: Mail.Options = {
from: user,
to: user, // Ensure this is the correct email address
subject: `Message from ${name} (${email})`,
text: message + "\n\n" + email,
};
const sendMailPromise = () =>
new Promise<string>((resolve, reject) => {
transport.sendMail(mailOptions, function (err, info) {
if (!err) {
resolve(info.response);
} else {
console.error("Error sending email:", err);
reject(err.message);
}
});
console.log('🔑 Environment check:', {
hasEmail: !!user,
hasPassword: !!pass,
emailHost: user.split('@')[1] || 'unknown'
});
try {
await sendMailPromise();
return NextResponse.json({ message: "Email sent" });
if (!user || !pass) {
console.error("❌ Missing email/password environment variables");
return NextResponse.json(
{ error: "E-Mail-Server nicht konfiguriert" },
{ status: 500 },
);
}
const transportOptions: SMTPTransport.Options = {
host: "smtp.ionos.de",
port: 587,
secure: false,
requireTLS: true,
auth: {
type: "login",
user,
pass,
},
// Add timeout and debug options
connectionTimeout: 10000,
greetingTimeout: 10000,
socketTimeout: 10000,
};
console.log('🚀 Creating transport with options:', {
host: transportOptions.host,
port: transportOptions.port,
secure: transportOptions.secure,
user: user.split('@')[0] + '@***' // Hide full email in logs
});
const transport = nodemailer.createTransport(transportOptions);
// Verify transport configuration
try {
await transport.verify();
console.log('✅ SMTP connection verified successfully');
} catch (verifyError) {
console.error('❌ SMTP verification failed:', verifyError);
return NextResponse.json(
{ error: "E-Mail-Server-Verbindung fehlgeschlagen" },
{ status: 500 },
);
}
const mailOptions: Mail.Options = {
from: `"Portfolio Contact" <${user}>`,
to: "contact@dki.one", // Send to your contact email
replyTo: email,
subject: `Portfolio Kontakt: ${subject}`,
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #3b82f6;">Neue Kontaktanfrage von deinem Portfolio</h2>
<div style="background: #f8fafc; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="color: #1e293b; margin-top: 0;">Nachricht von ${name}</h3>
<p style="color: #475569; margin: 8px 0;"><strong>E-Mail:</strong> ${email}</p>
<p style="color: #475569; margin: 8px 0;"><strong>Betreff:</strong> ${subject}</p>
</div>
<div style="background: #ffffff; padding: 20px; border-radius: 8px; border-left: 4px solid #3b82f6;">
<h4 style="color: #1e293b; margin-top: 0;">Nachricht:</h4>
<p style="color: #374151; line-height: 1.6; white-space: pre-wrap;">${message}</p>
</div>
<div style="text-align: center; margin-top: 30px; padding: 20px; background: #f1f5f9; border-radius: 8px;">
<p style="color: #64748b; margin: 0; font-size: 14px;">
Diese E-Mail wurde automatisch von deinem Portfolio generiert.
</p>
</div>
</div>
`,
text: `
Neue Kontaktanfrage von deinem Portfolio
Von: ${name} (${email})
Betreff: ${subject}
Nachricht:
${message}
---
Diese E-Mail wurde automatisch von deinem Portfolio generiert.
`,
};
console.log('📤 Sending email...');
const sendMailPromise = () =>
new Promise<string>((resolve, reject) => {
transport.sendMail(mailOptions, function (err, info) {
if (!err) {
console.log('✅ Email sent successfully:', info.response);
resolve(info.response);
} else {
console.error("❌ Error sending email:", err);
reject(err.message);
}
});
});
const result = await sendMailPromise();
console.log('🎉 Email process completed successfully');
return NextResponse.json({
message: "E-Mail erfolgreich gesendet",
messageId: result
});
} catch (err) {
console.error("Error sending email:", err);
return NextResponse.json({ error: "Failed to send email" }, { status: 500 });
console.error("❌ Unexpected error in email API:", err);
return NextResponse.json({
error: "Fehler beim Senden der E-Mail",
details: err instanceof Error ? err.message : 'Unbekannter Fehler'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = parseInt(params.id);
const project = await prisma.project.findUnique({
where: { id }
});
if (!project) {
return NextResponse.json(
{ error: 'Project not found' },
{ status: 404 }
);
}
return NextResponse.json(project);
} catch (error) {
console.error('Error fetching project:', error);
return NextResponse.json(
{ error: 'Failed to fetch project' },
{ status: 500 }
);
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = parseInt(params.id);
const data = await request.json();
const project = await prisma.project.update({
where: { id },
data: { ...data, updatedAt: new Date() }
});
return NextResponse.json(project);
} catch (error) {
console.error('Error updating project:', error);
return NextResponse.json(
{ error: 'Failed to update project' },
{ status: 500 }
);
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const id = parseInt(params.id);
await prisma.project.delete({
where: { id }
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error deleting project:', error);
return NextResponse.json(
{ error: 'Failed to delete project' },
{ status: 500 }
);
}
}

78
app/api/projects/route.ts Normal file
View File

@@ -0,0 +1,78 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '50');
const category = searchParams.get('category');
const featured = searchParams.get('featured');
const published = searchParams.get('published');
const difficulty = searchParams.get('difficulty');
const search = searchParams.get('search');
const skip = (page - 1) * limit;
const where: any = {};
if (category) where.category = category;
if (featured !== null) where.featured = featured === 'true';
if (published !== null) where.published = published === 'true';
if (difficulty) where.difficulty = difficulty;
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
{ tags: { hasSome: [search] } },
{ content: { contains: search, mode: 'insensitive' } }
];
}
const [projects, total] = await Promise.all([
prisma.project.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: limit
}),
prisma.project.count({ where })
]);
return NextResponse.json({
projects,
total,
pages: Math.ceil(total / limit),
currentPage: page
});
} catch (error) {
console.error('Error fetching projects:', error);
return NextResponse.json(
{ error: 'Failed to fetch projects' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const data = await request.json();
const project = await prisma.project.create({
data: {
...data,
performance: data.performance || { lighthouse: 90, bundleSize: '50KB', loadTime: '1.5s' },
analytics: data.analytics || { views: 0, likes: 0, shares: 0 }
}
});
return NextResponse.json(project);
} catch (error) {
console.error('Error creating project:', error);
return NextResponse.json(
{ error: 'Failed to create project' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,81 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const slug = searchParams.get('slug');
const search = searchParams.get('search');
const category = searchParams.get('category');
if (slug) {
// Search by slug (convert title to slug format)
const projects = await prisma.project.findMany({
where: {
published: true
},
orderBy: { createdAt: 'desc' }
});
// Find exact match by converting titles to slugs
const foundProject = projects.find(project => {
const projectSlug = project.title.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return projectSlug === slug;
});
if (foundProject) {
return NextResponse.json({ projects: [foundProject] });
}
// If no exact match, return empty array
return NextResponse.json({ projects: [] });
}
if (search) {
// General search
const projects = await prisma.project.findMany({
where: {
published: true,
OR: [
{ title: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
{ tags: { hasSome: [search] } },
{ content: { contains: search, mode: 'insensitive' } }
]
},
orderBy: { createdAt: 'desc' }
});
return NextResponse.json({ projects });
}
if (category && category !== 'All') {
// Filter by category
const projects = await prisma.project.findMany({
where: {
published: true,
category: category
},
orderBy: { createdAt: 'desc' }
});
return NextResponse.json({ projects });
}
// Return all published projects if no specific search
const projects = await prisma.project.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' }
});
return NextResponse.json({ projects });
} catch (error) {
console.error('Error searching projects:', error);
return NextResponse.json(
{ error: 'Failed to search projects' },
{ status: 500 }
);
}
}

View File

@@ -3,9 +3,11 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Mail, Phone, MapPin, Send, Github, Linkedin, Twitter } from 'lucide-react';
import { useToast } from '@/components/Toast';
const Contact = () => {
const [mounted, setMounted] = useState(false);
const { showEmailSent, showEmailError } = useToast();
useEffect(() => {
setMounted(true);
@@ -24,12 +26,33 @@ const Contact = () => {
e.preventDefault();
setIsSubmitting(true);
// Simulate form submission
setTimeout(() => {
try {
const response = await fetch('/api/email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: formData.name,
email: formData.email,
subject: formData.subject,
message: formData.message,
}),
});
if (response.ok) {
showEmailSent(formData.email);
setFormData({ name: '', email: '', subject: '', message: '' });
} else {
const errorData = await response.json();
showEmailError(errorData.error || 'Unbekannter Fehler');
}
} catch (error) {
console.error('Error sending email:', error);
showEmailError('Netzwerkfehler beim Senden der E-Mail');
} finally {
setIsSubmitting(false);
alert('Thank you for your message! I will get back to you soon.');
setFormData({ name: '', email: '', subject: '', message: '' });
}, 2000);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { ArrowDown, Code, Zap, Rocket } from 'lucide-react';
import Image from 'next/image';
const Hero = () => {
const [mounted, setMounted] = useState(false);
@@ -70,11 +71,72 @@ const Hero = () => {
</div>
<div className="relative z-10 text-center px-4 max-w-4xl mx-auto">
{/* Profile Image */}
<motion.div
initial={{ opacity: 0, scale: 0.8, rotateY: -15 }}
animate={{ opacity: 1, scale: 1, rotateY: 0 }}
transition={{ duration: 1, delay: 0.1, ease: "easeOut" }}
className="mb-8 flex justify-center"
>
<div className="relative group">
{/* Glowing border effect */}
<div className="absolute -inset-1 bg-gradient-to-r from-blue-600 via-purple-600 to-cyan-600 rounded-full blur opacity-75 group-hover:opacity-100 transition duration-1000 group-hover:duration-200 animate-pulse"></div>
{/* Profile image container */}
<div className="relative bg-gray-900 rounded-full p-1">
<motion.div
whileHover={{ scale: 1.05, rotateY: 5 }}
transition={{ duration: 0.3 }}
className="relative w-40 h-40 md:w-48 md:h-48 lg:w-56 lg:h-56 rounded-full overflow-hidden border-4 border-gray-800"
>
<Image
src="/images/me.jpg"
alt="Dennis Konkol - Software Engineer"
fill
className="object-cover"
priority
/>
{/* Hover overlay effect */}
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</motion.div>
</div>
{/* Floating tech badges around the image */}
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 1.5 }}
className="absolute -top-3 -right-3 w-10 h-10 bg-blue-500 rounded-full flex items-center justify-center shadow-lg"
>
<Code className="w-5 h-5 text-white" />
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 1.7 }}
className="absolute -bottom-3 -left-3 w-10 h-10 bg-purple-500 rounded-full flex items-center justify-center shadow-lg"
>
<Zap className="w-5 h-5 text-white" />
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 1.9 }}
className="absolute -top-3 -left-3 w-10 h-10 bg-cyan-500 rounded-full flex items-center justify-center shadow-lg"
>
<Rocket className="w-5 h-5 text-white" />
</motion.div>
</div>
</motion.div>
{/* Main Title */}
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
transition={{ duration: 0.8, delay: 0.8 }}
className="text-5xl md:text-7xl font-bold mb-6"
>
<span className="gradient-text">Dennis Konkol</span>
@@ -84,7 +146,7 @@ const Hero = () => {
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.4 }}
transition={{ duration: 0.8, delay: 1.0 }}
className="text-xl md:text-2xl text-gray-300 mb-8 max-w-2xl mx-auto"
>
Student & Software Engineer based in Osnabrück, Germany
@@ -94,7 +156,7 @@ const Hero = () => {
<motion.p
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.6 }}
transition={{ duration: 0.8, delay: 1.2 }}
className="text-lg text-gray-400 mb-12 max-w-3xl mx-auto leading-relaxed"
>
Passionate about technology, coding, and solving real-world problems.
@@ -105,7 +167,7 @@ const Hero = () => {
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.8 }}
transition={{ duration: 0.8, delay: 1.4 }}
className="flex flex-wrap justify-center gap-6 mb-12"
>
{features.map((feature, index) => (
@@ -113,7 +175,7 @@ const Hero = () => {
key={feature.text}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 1 + index * 0.1 }}
transition={{ duration: 0.5, delay: 1.6 + index * 0.1 }}
whileHover={{ scale: 1.05, y: -5 }}
className="flex items-center space-x-2 px-4 py-2 rounded-full glass-card"
>
@@ -127,7 +189,7 @@ const Hero = () => {
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 1.2 }}
transition={{ duration: 0.8, delay: 1.8 }}
className="flex flex-col sm:flex-row gap-4 justify-center items-center"
>
<motion.a

View File

@@ -27,12 +27,23 @@ const Projects = () => {
const [projects, setProjects] = useState<Project[]>([]);
// Load projects from localStorage
// Load projects from API
useEffect(() => {
const savedProjects = localStorage.getItem('portfolio-projects');
if (savedProjects) {
setProjects(JSON.parse(savedProjects));
}
const loadProjects = async () => {
try {
const response = await fetch('/api/projects?featured=true&published=true&limit=6');
if (response.ok) {
const data = await response.json();
setProjects(data.projects || []);
} else {
console.error('Failed to fetch projects from API');
}
} catch (error) {
console.error('Error loading projects:', error);
}
};
loadProjects();
}, []);
if (!mounted) {

View File

@@ -2,6 +2,7 @@ import "./globals.css";
import { Metadata } from "next";
import { Inter } from "next/font/google";
import React from "react";
import { ToastProvider } from "@/components/Toast";
const inter = Inter({
variable: "--font-inter",
@@ -24,7 +25,11 @@ export default function RootLayout({
<meta charSet="utf-8"/>
<title>Dennis Konkol&#39;s Portfolio</title>
</head>
<body className={inter.variable}>{children}</body>
<body className={inter.variable}>
<ToastProvider>
{children}
</ToastProvider>
</body>
</html>
);
}

View File

@@ -25,18 +25,25 @@ const ProjectDetail = () => {
const slug = params.slug as string;
const [project, setProject] = useState<Project | null>(null);
// Load project from localStorage by slug
// Load project from API by slug
useEffect(() => {
const savedProjects = localStorage.getItem('portfolio-projects');
if (savedProjects) {
const projects = JSON.parse(savedProjects);
const foundProject = projects.find((p: Project) =>
p.title.toLowerCase().replace(/[^a-z0-9]+/g, '-') === slug
);
if (foundProject) {
setProject(foundProject);
const loadProject = async () => {
try {
const response = await fetch(`/api/projects/search?slug=${slug}`);
if (response.ok) {
const data = await response.json();
if (data.projects && data.projects.length > 0) {
setProject(data.projects[0]);
}
} else {
console.error('Failed to fetch project from API');
}
} catch (error) {
console.error('Error loading project:', error);
}
}
};
loadProject();
}, [slug]);
if (!project) {

View File

@@ -22,12 +22,23 @@ interface Project {
const ProjectsPage = () => {
const [projects, setProjects] = useState<Project[]>([]);
// Load projects from localStorage
// Load projects from API
useEffect(() => {
const savedProjects = localStorage.getItem('portfolio-projects');
if (savedProjects) {
setProjects(JSON.parse(savedProjects));
}
const loadProjects = async () => {
try {
const response = await fetch('/api/projects?published=true');
if (response.ok) {
const data = await response.json();
setProjects(data.projects || []);
} else {
console.error('Failed to fetch projects from API');
}
} catch (error) {
console.error('Error loading projects:', error);
}
};
loadProjects();
}, []);
const categories = ["All", "Web Development", "Full-Stack", "Web Application", "Mobile App"];