349 lines
13 KiB
TypeScript
349 lines
13 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import {
|
|
Plus,
|
|
Edit,
|
|
Trash2,
|
|
Search,
|
|
Grid,
|
|
List,
|
|
Globe,
|
|
Github,
|
|
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 {
|
|
projects: Project[];
|
|
onProjectsChange: () => void;
|
|
}
|
|
|
|
export const ProjectManager: React.FC<ProjectManagerProps> = ({
|
|
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'];
|
|
|
|
// 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>
|
|
);
|
|
};
|