Files
portfolio/components/ProjectManager.tsx
denshooter be01ee2adb 🔧 Enhance Middleware and Admin Features
 Updated Middleware Logic:
- Enhanced admin route protection with Basic Auth for legacy routes and session-based auth for `/manage` and `/editor`.

 Improved Admin Panel Styles:
- Added glassmorphism styles for admin components to enhance UI aesthetics.

 Refined Rate Limiting:
- Adjusted rate limits for admin dashboard requests to allow more generous access.

 Introduced Analytics Reset API:
- Added a new endpoint for resetting analytics data with rate limiting and admin authentication.

🎯 Overall Improvements:
- Strengthened security and user experience for admin functionalities.
- Enhanced visual design for better usability.
- Streamlined analytics management processes.
2025-09-09 19:50:52 +02:00

365 lines
13 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Plus,
Edit,
Trash2,
Eye,
Search,
Filter,
Grid,
List,
Save,
X,
Upload,
Image as ImageIcon,
Link as LinkIcon,
Globe,
Github,
Calendar,
Tag,
Star,
TrendingUp,
Settings,
MoreVertical,
RefreshCw
} from 'lucide-react';
// Editor is now a separate page at /editor
interface Project {
id: string;
title: string;
description: string;
content?: string;
category: string;
difficulty?: string;
tags?: string[];
featured: boolean;
published: boolean;
github?: string;
live?: string;
image?: string;
createdAt: string;
updatedAt: string;
analytics?: {
views: number;
likes: number;
shares: number;
};
performance?: {
lighthouse: number;
};
}
interface ProjectManagerProps {
isAuthenticated: boolean;
projects: Project[];
onProjectsChange: () => void;
}
export const ProjectManager: React.FC<ProjectManagerProps> = ({
isAuthenticated,
projects,
onProjectsChange
}) => {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
// Editor is now a separate page - no modal state needed
const categories = ['all', 'Web Development', 'Full-Stack', 'Web Application', 'Mobile App', 'Design'];
const difficulties = ['Beginner', 'Intermediate', 'Advanced', 'Expert'];
// Filter projects
const filteredProjects = projects.filter((project) => {
const matchesSearch =
project.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.tags?.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesCategory = selectedCategory === 'all' || project.category === selectedCategory;
return matchesSearch && matchesCategory;
});
const openEditor = (project?: Project) => {
// Simple navigation to editor - let the editor handle auth
if (project) {
window.location.href = `/editor?id=${project.id}`;
} else {
window.location.href = '/editor';
}
};
// closeEditor removed - editor is now separate page
// saveProject removed - editor is now separate page
const deleteProject = async (projectId: string) => {
if (!confirm('Are you sure you want to delete this project?')) return;
try {
await fetch(`/api/projects/${projectId}`, {
method: 'DELETE',
headers: {
'x-admin-request': 'true'
}
});
onProjectsChange();
} catch (error) {
console.error('Error deleting project:', error);
}
};
const getStatusColor = (project: Project) => {
if (project.published) {
return project.featured ? 'text-purple-400 bg-purple-500/20' : 'text-green-400 bg-green-500/20';
}
return 'text-yellow-400 bg-yellow-500/20';
};
const getStatusText = (project: Project) => {
if (project.published) {
return project.featured ? 'Featured' : 'Published';
}
return 'Draft';
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-white">Project Management</h1>
<p className="text-white/80">{projects.length} projects {projects.filter(p => p.published).length} published</p>
</div>
<div className="flex items-center space-x-3">
<button
onClick={onProjectsChange}
className="flex items-center space-x-2 px-4 py-2 admin-glass-light rounded-xl hover:scale-105 transition-all duration-200"
>
<RefreshCw className="w-4 h-4 text-blue-400" />
<span className="text-white font-medium">Refresh</span>
</button>
<button
onClick={() => openEditor()}
className="flex items-center space-x-2 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-xl hover:scale-105 transition-all duration-200 shadow-lg"
>
<Plus size={18} />
<span className="font-medium">New Project</span>
</button>
</div>
</div>
{/* Filters and View Toggle */}
<div className="flex flex-col sm:flex-row gap-4">
{/* Search */}
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-white/60" />
<input
type="text"
placeholder="Search projects..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-3 admin-glass-light border border-white/30 rounded-xl text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Category Filter */}
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
className="px-4 py-3 admin-glass-light border border-white/30 rounded-xl text-white focus:outline-none focus:ring-2 focus:ring-blue-500 bg-transparent"
>
{categories.map(category => (
<option key={category} value={category} className="bg-gray-800">
{category === 'all' ? 'All Categories' : category}
</option>
))}
</select>
{/* View Toggle */}
<div className="flex items-center space-x-1 admin-glass-light rounded-xl p-1">
<button
onClick={() => setViewMode('grid')}
className={`p-2 rounded-lg transition-all duration-200 ${
viewMode === 'grid'
? 'bg-blue-500/40 text-blue-300'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
>
<Grid className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('list')}
className={`p-2 rounded-lg transition-all duration-200 ${
viewMode === 'list'
? 'bg-blue-500/40 text-blue-300'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
>
<List className="w-4 h-4" />
</button>
</div>
</div>
{/* Projects Grid/List */}
{viewMode === 'grid' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{filteredProjects.map((project) => (
<motion.div
key={project.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="admin-glass-card p-6 rounded-xl hover:scale-105 transition-all duration-300 group"
>
{/* Project Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-xl font-bold text-white mb-1">{project.title}</h3>
<p className="text-white/70 text-sm">{project.category}</p>
</div>
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => openEditor(project)}
className="p-2 text-white/70 hover:text-blue-400 hover:bg-white/10 rounded-lg transition-colors"
>
<Edit size={16} />
</button>
<button
onClick={() => deleteProject(project.id)}
className="p-2 text-white/70 hover:text-red-400 hover:bg-white/10 rounded-lg transition-colors"
>
<Trash2 size={16} />
</button>
</div>
</div>
{/* Project Content */}
<div className="space-y-4">
<div>
<p className="text-white/70 text-sm line-clamp-2 leading-relaxed">{project.description}</p>
</div>
{/* Tags */}
{project.tags && project.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{project.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="px-2 py-1 bg-white/10 text-white/70 rounded-full text-xs"
>
{tag}
</span>
))}
{project.tags.length > 3 && (
<span className="px-2 py-1 bg-white/10 text-white/70 rounded-full text-xs">
+{project.tags.length - 3}
</span>
)}
</div>
)}
{/* Status and Links */}
<div className="flex items-center justify-between">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(project)}`}>
{getStatusText(project)}
</span>
<div className="flex items-center space-x-1">
{project.github && (
<a
href={project.github}
target="_blank"
rel="noopener noreferrer"
className="p-1 text-white/60 hover:text-white transition-colors"
>
<Github size={14} />
</a>
)}
{project.live && (
<a
href={project.live}
target="_blank"
rel="noopener noreferrer"
className="p-1 text-white/60 hover:text-white transition-colors"
>
<Globe size={14} />
</a>
)}
</div>
</div>
{/* Analytics */}
<div className="grid grid-cols-3 gap-2 pt-3 border-t border-white/10">
<div className="text-center">
<p className="text-white font-bold text-sm">{project.analytics?.views || 0}</p>
<p className="text-white/60 text-xs">Views</p>
</div>
<div className="text-center">
<p className="text-white font-bold text-sm">{project.analytics?.likes || 0}</p>
<p className="text-white/60 text-xs">Likes</p>
</div>
<div className="text-center">
<p className="text-white font-bold text-sm">{project.performance?.lighthouse || 90}</p>
<p className="text-white/60 text-xs">Score</p>
</div>
</div>
</div>
</motion.div>
))}
</div>
) : (
<div className="space-y-4">
{filteredProjects.map((project) => (
<motion.div
key={project.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="admin-glass-card p-6 rounded-xl hover:scale-[1.01] transition-all duration-300 group"
>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className="flex-1">
<h3 className="text-white font-bold text-lg">{project.title}</h3>
<p className="text-white/70 text-sm">{project.category}</p>
</div>
</div>
<div className="flex items-center space-x-4">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(project)}`}>
{getStatusText(project)}
</span>
<div className="flex items-center space-x-3 text-white/60 text-sm">
<span>{project.analytics?.views || 0} views</span>
<span></span>
<span>{new Date(project.updatedAt).toLocaleDateString()}</span>
</div>
<div className="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={() => openEditor(project)}
className="p-2 text-white/70 hover:text-blue-400 hover:bg-white/10 rounded-lg transition-colors"
>
<Edit size={16} />
</button>
<button
onClick={() => deleteProject(project.id)}
className="p-2 text-white/70 hover:text-red-400 hover:bg-white/10 rounded-lg transition-colors"
>
<Trash2 size={16} />
</button>
</div>
</div>
</div>
</motion.div>
))}
</div>
)}
{/* Editor is now a separate page at /editor */}
</div>
);
};