Merge dev branch into production - resolve conflicts

- Updated admin URLs from /admin to /manage
- Integrated new admin dashboard and email management features
- Added authentication system and project management
- Resolved conflicts in DEV-SETUP.md, README.md, email routes, and components
- Removed old admin page in favor of new manage page
This commit is contained in:
2025-09-10 11:06:36 +02:00
37 changed files with 5746 additions and 423 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import {
BarChart3,
@@ -8,12 +8,15 @@ import {
Eye,
Heart,
Zap,
Users,
Clock,
Globe,
Activity,
Target,
Award
Award,
RefreshCw,
MousePointer,
RotateCcw,
Trash2,
AlertTriangle
} from 'lucide-react';
interface AnalyticsData {
@@ -48,48 +51,41 @@ interface AnalyticsData {
totalLikes: number;
totalShares: number;
};
metrics: {
bounceRate: number;
avgSessionDuration: number;
pagesPerSession: number;
newUsers: number;
};
}
interface PerformanceData {
pageViews: {
total: number;
last24h: number;
last7d: number;
last30d: number;
};
interactions: {
total: number;
last24h: number;
last7d: number;
last30d: number;
};
topPages: Record<string, number>;
topInteractions: Record<string, number>;
interface AnalyticsDashboardProps {
isAuthenticated: boolean;
}
export function AnalyticsDashboard() {
const [analyticsData, setAnalyticsData] = useState<AnalyticsData | null>(null);
const [performanceData, setPerformanceData] = useState<PerformanceData | null>(null);
const [loading, setLoading] = useState(true);
export function AnalyticsDashboard({ isAuthenticated }: AnalyticsDashboardProps) {
const [data, setData] = useState<AnalyticsData | null>(null);
const [loading, setLoading] = useState(false);
>>>>>>> dev
const [error, setError] = useState<string | null>(null);
const [timeRange, setTimeRange] = useState<'7d' | '30d' | '90d' | '1y'>('30d');
const [showResetModal, setShowResetModal] = useState(false);
const [resetType, setResetType] = useState<'analytics' | 'pageviews' | 'interactions' | 'performance' | 'all'>('analytics');
const [resetting, setResetting] = useState(false);
useEffect(() => {
fetchAnalyticsData();
}, []);
const fetchAnalyticsData = async () => {
const fetchAnalyticsData = useCallback(async () => {
if (!isAuthenticated) return;
try {
setLoading(true);
// Get basic auth from environment or use default
const auth = btoa('admin:change_this_password_123');
setError(null);
const [analyticsRes, performanceRes] = await Promise.all([
fetch('/api/analytics/dashboard', {
headers: { 'Authorization': `Basic ${auth}` }
headers: { 'x-admin-request': 'true' }
}),
fetch('/api/analytics/performance', {
headers: { 'Authorization': `Basic ${auth}` }
headers: { 'x-admin-request': 'true' }
})
]);
@@ -102,272 +98,486 @@ export function AnalyticsDashboard() {
performanceRes.json()
]);
setAnalyticsData(analytics);
setPerformanceData(performance);
setData({
overview: analytics.overview || {
totalProjects: 0,
publishedProjects: 0,
featuredProjects: 0,
totalViews: 0,
totalLikes: 0,
totalShares: 0,
avgLighthouse: 90
},
projects: analytics.projects || [],
categories: analytics.categories || {},
difficulties: analytics.difficulties || {},
performance: performance.performance || {
avgLighthouse: 90,
totalViews: 0,
totalLikes: 0,
totalShares: 0
},
metrics: performance.metrics || {
bounceRate: 35,
avgSessionDuration: 180,
pagesPerSession: 2.5,
newUsers: 0
}
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
setError(err instanceof Error ? err.message : 'Failed to load analytics');
} finally {
setLoading(false);
}
}, [isAuthenticated]);
const resetAnalytics = async () => {
if (!isAuthenticated || resetting) return;
setResetting(true);
try {
const response = await fetch('/api/analytics/reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-admin-request': 'true'
},
body: JSON.stringify({ type: resetType })
});
if (response.ok) {
await fetchAnalyticsData(); // Refresh data
setShowResetModal(false);
} else {
const errorData = await response.json();
setError(errorData.error || 'Failed to reset analytics');
}
} catch (err) {
setError('Failed to reset analytics');
console.error('Reset error:', err);
} finally {
setResetting(false);
}
};
if (loading) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div className="animate-pulse">
<div className="h-6 bg-gray-300 dark:bg-gray-700 rounded w-1/4 mb-4"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-gray-300 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
</div>
);
}
useEffect(() => {
if (isAuthenticated) {
fetchAnalyticsData();
}
}, [isAuthenticated, fetchAnalyticsData]);
if (error) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div className="text-center text-red-500">
<p>Error loading analytics: {error}</p>
<button
onClick={fetchAnalyticsData}
className="mt-2 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Retry
</button>
</div>
</div>
);
}
if (!analyticsData || !performanceData) return null;
const StatCard = ({ title, value, icon: Icon, color, trend }: {
const StatCard = ({ title, value, icon: Icon, color, trend, trendValue, description }: {
title: string;
value: number | string;
icon: React.ComponentType<{ className?: string }>;
icon: React.ComponentType<{ className?: string; size?: number }>;
color: string;
trend?: string;
trend?: 'up' | 'down' | 'neutral';
trendValue?: string;
description?: string;
}) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-700 dark:to-gray-800 rounded-xl p-6 border border-gray-200 dark:border-gray-600"
className="admin-glass-card p-6 rounded-xl hover:scale-105 transition-all duration-200"
>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{title}</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{value}</p>
{trend && (
<p className="text-xs text-green-600 dark:text-green-400 flex items-center mt-1">
<TrendingUp className="w-3 h-3 mr-1" />
{trend}
</p>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center space-x-3 mb-4">
<div className={`p-3 rounded-xl ${color}`}>
<Icon className="w-6 h-6 text-white" size={24} />
</div>
<div>
<p className="text-white/60 text-sm font-medium">{title}</p>
{description && <p className="text-white/40 text-xs">{description}</p>}
</div>
</div>
<p className="text-3xl font-bold text-white mb-2">{value}</p>
{trend && trendValue && (
<div className={`flex items-center space-x-1 text-sm ${
trend === 'up' ? 'text-green-400' :
trend === 'down' ? 'text-red-400' : 'text-yellow-400'
}`}>
<TrendingUp className={`w-4 h-4 ${trend === 'down' ? 'rotate-180' : ''}`} />
<span>{trendValue}</span>
</div>
)}
</div>
<div className={`p-3 rounded-lg ${color}`}>
<Icon className="w-6 h-6 text-white" />
</div>
</div>
</motion.div>
);
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'Beginner': return 'bg-green-500';
case 'Intermediate': return 'bg-yellow-500';
case 'Advanced': return 'bg-orange-500';
case 'Expert': return 'bg-red-500';
default: return 'bg-gray-500';
case 'Beginner': return 'bg-green-500/30 text-green-400 border-green-500/40';
case 'Intermediate': return 'bg-yellow-500/30 text-yellow-400 border-yellow-500/40';
case 'Advanced': return 'bg-orange-500/30 text-orange-400 border-orange-500/40';
case 'Expert': return 'bg-red-500/30 text-red-400 border-red-500/40';
default: return 'bg-gray-500/30 text-gray-400 border-gray-500/40';
}
};
const getCategoryColor = (index: number) => {
const colors = [
'bg-blue-500/30 text-blue-400',
'bg-purple-500/30 text-purple-400',
'bg-green-500/30 text-green-400',
'bg-pink-500/30 text-pink-400',
'bg-indigo-500/30 text-indigo-400'
];
return colors[index % colors.length];
};
if (!isAuthenticated) {
return (
<div className="admin-glass-card p-8 rounded-xl text-center">
<BarChart3 className="w-16 h-16 text-white/40 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-white mb-2">Authentication Required</h3>
<p className="text-white/60">Please log in to view analytics data</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="space-y-8">
{/* Header */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
<BarChart3 className="w-6 h-6 mr-2 text-blue-600" />
Analytics Dashboard
</h2>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Übersicht über deine Portfolio-Performance
</p>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-white flex items-center">
<BarChart3 className="w-8 h-8 mr-3 text-blue-400" />
Analytics Dashboard
</h1>
<p className="text-white/80 mt-2">Portfolio performance and user engagement metrics</p>
</div>
<div className="flex items-center space-x-3">
{/* Time Range Selector */}
<div className="flex items-center space-x-1 admin-glass-light rounded-xl p-1">
{(['7d', '30d', '90d', '1y'] as const).map((range) => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
timeRange === range
? 'bg-blue-500/40 text-blue-300 shadow-lg'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
>
{range === '7d' ? '7 Days' : range === '30d' ? '30 Days' : range === '90d' ? '90 Days' : '1 Year'}
</button>
))}
</div>
<button
onClick={fetchAnalyticsData}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
disabled={loading}
className="flex items-center space-x-2 px-4 py-2 admin-glass-light rounded-xl hover:scale-105 transition-all duration-200 disabled:opacity-50"
>
Refresh
<RefreshCw className={`w-4 h-4 text-blue-400 ${loading ? 'animate-spin' : ''}`} />
<span className="text-white font-medium">Refresh</span>
</button>
<button
onClick={() => setShowResetModal(true)}
className="flex items-center space-x-2 px-4 py-2 bg-red-600/20 text-red-400 border border-red-500/30 rounded-xl hover:bg-red-600/30 hover:scale-105 transition-all"
>
<RotateCcw className="w-4 h-4" />
<span>Reset</span>
</button>
</div>
</div>
{/* Overview Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="Total Projects"
value={analyticsData.overview.totalProjects}
icon={Target}
color="bg-blue-500"
/>
<StatCard
title="Total Views"
value={analyticsData.overview.totalViews.toLocaleString()}
icon={Eye}
color="bg-green-500"
/>
<StatCard
title="Total Likes"
value={analyticsData.overview.totalLikes.toLocaleString()}
icon={Heart}
color="bg-red-500"
/>
<StatCard
title="Avg Lighthouse"
value={analyticsData.overview.avgLighthouse}
icon={Zap}
color="bg-yellow-500"
/>
</div>
{loading && (
<div className="admin-glass-card p-8 rounded-xl">
<div className="flex items-center justify-center space-x-3">
<RefreshCw className="w-6 h-6 text-blue-400 animate-spin" />
<span className="text-white/80 text-lg">Loading analytics data...</span>
</div>
</div>
)}
{/* Performance Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
title="Views (24h)"
value={performanceData.pageViews.last24h}
icon={Activity}
color="bg-purple-500"
/>
<StatCard
title="Views (7d)"
value={performanceData.pageViews.last7d}
icon={Clock}
color="bg-indigo-500"
/>
<StatCard
title="Interactions (24h)"
value={performanceData.interactions.last24h}
icon={Users}
color="bg-pink-500"
/>
<StatCard
title="Interactions (7d)"
value={performanceData.interactions.last7d}
icon={Globe}
color="bg-teal-500"
/>
</div>
{error && (
<div className="admin-glass-card p-6 rounded-xl border border-red-500/40">
<div className="flex items-center space-x-3 text-red-300">
<Activity className="w-5 h-5" />
<span>Error: {error}</span>
</div>
</div>
)}
{/* Projects Performance */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<Award className="w-5 h-5 mr-2 text-yellow-500" />
Top Performing Projects
</h3>
<div className="space-y-4">
{analyticsData.projects
.sort((a, b) => b.views - a.views)
.slice(0, 5)
.map((project, index) => (
<motion.div
key={project.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg"
{data && !loading && (
<>
{/* Overview Stats */}
<div>
<h2 className="text-xl font-bold text-white mb-6 flex items-center">
<Target className="w-5 h-5 mr-2 text-purple-400" />
Overview
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
<StatCard
title="Total Views"
value={data.overview.totalViews.toLocaleString()}
icon={Eye}
color="bg-blue-500/30"
trend="up"
trendValue="+12.5%"
description="All-time page views"
/>
<StatCard
title="Projects"
value={data.overview.totalProjects}
icon={Globe}
color="bg-green-500/30"
trend="up"
trendValue="+2"
description={`${data.overview.publishedProjects} published`}
/>
<StatCard
title="Engagement"
value={data.overview.totalLikes}
icon={Heart}
color="bg-pink-500/30"
trend="up"
trendValue="+8.2%"
description="Total likes & shares"
/>
<StatCard
title="Performance"
value={data.overview.avgLighthouse}
icon={Zap}
color="bg-orange-500/30"
trend="up"
trendValue="+5%"
description="Avg Lighthouse score"
/>
<StatCard
title="Bounce Rate"
value={`${data.metrics.bounceRate}%`}
icon={MousePointer}
color="bg-purple-500/30"
trend="down"
trendValue="-2.1%"
description="User retention"
/>
</div>
</div>
{/* Project Performance */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Top Projects */}
<div className="admin-glass-card p-6 rounded-xl">
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
<Award className="w-5 h-5 mr-2 text-yellow-400" />
Top Performing Projects
</h3>
<div className="space-y-4">
{data.projects
.sort((a, b) => b.views - a.views)
.slice(0, 5)
.map((project, index) => (
<motion.div
key={project.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center justify-between p-4 admin-glass-light rounded-xl"
>
<div className="flex items-center space-x-4">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-500 rounded-lg flex items-center justify-center text-white font-bold">
#{index + 1}
</div>
<div>
<p className="text-white font-medium">{project.title}</p>
<p className="text-white/60 text-sm">{project.category}</p>
</div>
</div>
<div className="text-right">
<p className="text-white font-bold">{project.views.toLocaleString()}</p>
<p className="text-white/60 text-sm">views</p>
</div>
</motion.div>
))}
</div>
</div>
{/* Categories Distribution */}
<div className="admin-glass-card p-6 rounded-xl">
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
<BarChart3 className="w-5 h-5 mr-2 text-green-400" />
Categories
</h3>
<div className="space-y-4">
{Object.entries(data.categories).map(([category, count], index) => (
<motion.div
key={category}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center justify-between"
>
<div className="flex items-center space-x-3">
<div className={`w-4 h-4 rounded-full ${getCategoryColor(index)}`}></div>
<span className="text-white font-medium">{category}</span>
</div>
<div className="flex items-center space-x-3">
<div className="w-32 h-2 bg-white/10 rounded-full overflow-hidden">
<div
className={`h-full ${getCategoryColor(index)} transition-all duration-500`}
style={{ width: `${(count / Math.max(...Object.values(data.categories))) * 100}%` }}
></div>
</div>
<span className="text-white/80 font-medium w-8 text-right">{count}</span>
</div>
</motion.div>
))}
</div>
</div>
</div>
{/* Difficulty & Engagement */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Difficulty Distribution */}
<div className="admin-glass-card p-6 rounded-xl">
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
<Target className="w-5 h-5 mr-2 text-red-400" />
Difficulty Levels
</h3>
<div className="grid grid-cols-2 gap-4">
{Object.entries(data.difficulties).map(([difficulty, count]) => (
<motion.div
key={difficulty}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className={`p-4 rounded-xl border ${getDifficultyColor(difficulty)}`}
>
<div className="text-center">
<p className="text-2xl font-bold mb-1">{count}</p>
<p className="text-sm font-medium">{difficulty}</p>
</div>
</motion.div>
))}
</div>
</div>
{/* Recent Activity */}
<div className="admin-glass-card p-6 rounded-xl">
<h3 className="text-xl font-bold text-white mb-6 flex items-center">
<Activity className="w-5 h-5 mr-2 text-blue-400" />
Recent Activity
</h3>
<div className="space-y-4">
{data.projects
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
.slice(0, 4)
.map((project, index) => (
<motion.div
key={project.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center space-x-4 p-3 admin-glass-light rounded-xl"
>
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<div className="flex-1">
<p className="text-white font-medium text-sm">{project.title}</p>
<p className="text-white/60 text-xs">
Updated {new Date(project.updatedAt).toLocaleDateString()}
</p>
</div>
<div className="flex items-center space-x-2">
{project.featured && (
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 rounded-full text-xs">
Featured
</span>
)}
<span className={`px-2 py-1 rounded-full text-xs ${
project.published
? 'bg-green-500/20 text-green-400'
: 'bg-yellow-500/20 text-yellow-400'
}`}>
{project.published ? 'Live' : 'Draft'}
</span>
</div>
</motion.div>
))}
</div>
</div>
</div>
</>
)}
{/* Reset Modal */}
{showResetModal && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="admin-glass-card rounded-2xl p-6 w-full max-w-md"
>
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-red-500/20 rounded-lg flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-red-400" />
</div>
<div>
<h3 className="text-lg font-bold text-white">Reset Analytics Data</h3>
<p className="text-white/60 text-sm">This action cannot be undone</p>
</div>
</div>
<div className="space-y-4 mb-6">
<div>
<label className="block text-white/80 text-sm mb-2">Reset Type</label>
<select
value={resetType}
onChange={(e) => setResetType(e.target.value as 'all' | 'performance' | 'analytics')}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-red-500"
>
<option value="analytics">Analytics Only (views, likes, shares)</option>
<option value="pageviews">Page Views Only</option>
<option value="interactions">User Interactions Only</option>
<option value="performance">Performance Metrics Only</option>
<option value="all">Everything (Complete Reset)</option>
</select>
</div>
<div className="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
<div className="flex items-start space-x-2">
<AlertTriangle className="w-4 h-4 text-red-400 mt-0.5 flex-shrink-0" />
<div className="text-sm text-red-300">
<p className="font-medium mb-1">Warning:</p>
<p>This will permanently delete the selected analytics data. This action cannot be reversed.</p>
</div>
</div>
</div>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => setShowResetModal(false)}
disabled={resetting}
className="flex-1 px-4 py-2 admin-glass-light text-white rounded-lg hover:scale-105 transition-all disabled:opacity-50"
>
<div className="flex items-center space-x-4">
<div className="w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center font-bold">
{index + 1}
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white">{project.title}</h4>
<div className="flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400">
<span className={`px-2 py-1 rounded text-xs text-white ${getDifficultyColor(project.difficulty)}`}>
{project.difficulty}
</span>
<span>{project.category}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-6 text-sm">
<div className="text-center">
<p className="font-medium text-gray-900 dark:text-white">{project.views}</p>
<p className="text-gray-600 dark:text-gray-400">Views</p>
</div>
<div className="text-center">
<p className="font-medium text-gray-900 dark:text-white">{project.likes}</p>
<p className="text-gray-600 dark:text-gray-400">Likes</p>
</div>
<div className="text-center">
<p className="font-medium text-gray-900 dark:text-white">{project.lighthouse}</p>
<p className="text-gray-600 dark:text-gray-400">Lighthouse</p>
</div>
</div>
</motion.div>
))}
Cancel
</button>
<button
onClick={resetAnalytics}
disabled={resetting}
className="flex-1 flex items-center justify-center space-x-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg hover:scale-105 transition-all disabled:opacity-50"
>
{resetting ? (
<>
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>Resetting...</span>
</>
) : (
<>
<Trash2 className="w-4 h-4" />
<span>Reset {resetType === 'all' ? 'Everything' : 'Data'}</span>
</>
)}
</button>
</div>
</motion.div>
</div>
</div>
{/* Categories & Difficulties */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Projects by Category
</h3>
<div className="space-y-3">
{Object.entries(analyticsData.categories)
.sort(([,a], [,b]) => b - a)
.map(([category, count]) => (
<div key={category} className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-300">{category}</span>
<div className="flex items-center space-x-2">
<div className="w-20 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${(count / analyticsData.overview.totalProjects) * 100}%` }}
></div>
</div>
<span className="text-sm font-medium text-gray-900 dark:text-white w-8 text-right">
{count}
</span>
</div>
</div>
))}
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Projects by Difficulty
</h3>
<div className="space-y-3">
{Object.entries(analyticsData.difficulties)
.sort(([,a], [,b]) => b - a)
.map(([difficulty, count]) => (
<div key={difficulty} className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-300">{difficulty}</span>
<div className="flex items-center space-x-2">
<div className="w-20 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
<div
className={`h-2 rounded-full ${getDifficultyColor(difficulty)}`}
style={{ width: `${(count / analyticsData.overview.totalProjects) * 100}%` }}
></div>
</div>
<span className="text-sm font-medium text-gray-900 dark:text-white w-8 text-right">
{count}
</span>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
);
}

View File

@@ -50,7 +50,7 @@ export const AnalyticsProvider: React.FC<AnalyticsProviderProps> = ({ children }
trackEvent('click', {
element,
className: className ? className.split(' ')[0] : undefined,
className: (typeof className === 'string' && className) ? className.split(' ')[0] : undefined,
id: id || undefined,
url: window.location.pathname,
});

View File

@@ -1,6 +1,7 @@
'use client';
import React, { useState, useEffect } from 'react';
<<<<<<< HEAD
import { EmailResponder } from './EmailResponder';
interface ContactMessage {
@@ -248,3 +249,366 @@ export const EmailManager: React.FC = () => {
</div>
);
};
=======
import { motion, AnimatePresence } from 'framer-motion';
import {
Mail,
Search,
Reply,
User,
CheckCircle,
Circle,
Send,
X,
RefreshCw,
Eye,
Calendar,
AtSign
} from 'lucide-react';
interface ContactMessage {
id: string;
name: string;
email: string;
subject: string;
message: string;
createdAt: string;
read: boolean;
responded: boolean;
priority: 'low' | 'medium' | 'high';
}
export const EmailManager: React.FC = () => {
const [messages, setMessages] = useState<ContactMessage[]>([]);
const [selectedMessage, setSelectedMessage] = useState<ContactMessage | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState<'all' | 'unread' | 'responded'>('all');
const [searchTerm, setSearchTerm] = useState('');
const [showReplyModal, setShowReplyModal] = useState(false);
const [replyContent, setReplyContent] = useState('');
// Load messages from API
const loadMessages = async () => {
try {
setIsLoading(true);
const response = await fetch('/api/contacts', {
headers: {
'x-admin-request': 'true'
}
});
if (response.ok) {
const data = await response.json();
const formattedMessages = data.contacts.map((contact: ContactMessage) => ({
id: contact.id.toString(),
name: contact.name,
email: contact.email,
subject: contact.subject,
message: contact.message,
createdAt: contact.createdAt,
read: false,
responded: contact.responded || false,
priority: 'medium' as const
}));
setMessages(formattedMessages);
}
} catch (error) {
console.error('Error loading messages:', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
loadMessages();
}, []);
const filteredMessages = messages.filter(message => {
const matchesFilter = filter === 'all' ||
(filter === 'unread' && !message.read) ||
(filter === 'responded' && message.responded);
const matchesSearch = searchTerm === '' ||
message.subject.toLowerCase().includes(searchTerm.toLowerCase()) ||
message.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
message.email.toLowerCase().includes(searchTerm.toLowerCase());
return matchesFilter && matchesSearch;
});
const handleMessageClick = (message: ContactMessage) => {
setSelectedMessage(message);
// Mark as read
setMessages(prev => prev.map(msg =>
msg.id === message.id ? { ...msg, read: true } : msg
));
};
const handleReply = async () => {
if (!selectedMessage || !replyContent.trim()) return;
try {
const response = await fetch('/api/email/respond', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
to: selectedMessage.email,
name: selectedMessage.name,
template: 'reply',
originalMessage: selectedMessage.message,
response: replyContent
})
});
if (response.ok) {
setMessages(prev => prev.map(msg =>
msg.id === selectedMessage.id ? { ...msg, responded: true } : msg
));
setShowReplyModal(false);
setReplyContent('');
}
} catch (error) {
console.error('Error sending reply:', error);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getPriorityColor = (priority: string) => {
switch (priority) {
case 'high': return 'text-red-400';
case 'medium': return 'text-yellow-400';
case 'low': return 'text-green-400';
default: return 'text-blue-400';
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full"
/>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Email Manager</h2>
<p className="text-white/70 mt-1">Manage your contact messages</p>
</div>
<button
onClick={loadMessages}
className="flex items-center space-x-2 px-4 py-2 bg-blue-500/20 text-blue-400 rounded-lg hover:bg-blue-500/30 transition-colors"
>
<RefreshCw className="w-4 h-4" />
<span>Refresh</span>
</button>
</div>
{/* Filters and Search */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/50 w-4 h-4" />
<input
type="text"
placeholder="Search messages..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex space-x-2">
{['all', 'unread', 'responded'].map((filterType) => (
<button
key={filterType}
onClick={() => setFilter(filterType as 'all' | 'unread' | 'responded')}
className={`px-4 py-2 rounded-lg transition-colors ${
filter === filterType
? 'bg-blue-500 text-white'
: 'bg-white/10 text-white/70 hover:bg-white/20'
}`}
>
{filterType.charAt(0).toUpperCase() + filterType.slice(1)}
</button>
))}
</div>
</div>
{/* Messages List */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-1 space-y-3">
{filteredMessages.length === 0 ? (
<div className="text-center py-12 text-white/50">
<Mail className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>No messages found</p>
</div>
) : (
filteredMessages.map((message) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`p-4 rounded-lg cursor-pointer transition-all ${
selectedMessage?.id === message.id
? 'bg-blue-500/20 border border-blue-500/50'
: 'bg-white/5 border border-white/10 hover:bg-white/10'
}`}
onClick={() => handleMessageClick(message)}
>
<div className="flex items-start justify-between mb-2">
<h3 className="font-semibold text-white truncate">{message.subject}</h3>
<div className="flex items-center space-x-2">
{!message.read && <Circle className="w-3 h-3 text-blue-400" />}
{message.responded && <CheckCircle className="w-3 h-3 text-green-400" />}
</div>
</div>
<p className="text-white/70 text-sm mb-2">{message.name}</p>
<p className="text-white/50 text-xs">{formatDate(message.createdAt)}</p>
</motion.div>
))
)}
</div>
{/* Message Detail */}
<div className="lg:col-span-2 admin-glass-card p-6 rounded-xl">
{selectedMessage ? (
<div className="space-y-6">
{/* Message Header */}
<div className="flex items-start justify-between">
<div className="space-y-2">
<h3 className="text-xl font-bold text-white">{selectedMessage.subject}</h3>
<div className="flex items-center space-x-4 text-sm text-white/70">
<div className="flex items-center space-x-2">
<User className="w-4 h-4" />
<span>{selectedMessage.name}</span>
</div>
<div className="flex items-center space-x-2">
<AtSign className="w-4 h-4" />
<span>{selectedMessage.email}</span>
</div>
<div className="flex items-center space-x-2">
<Calendar className="w-4 h-4" />
<span>{formatDate(selectedMessage.createdAt)}</span>
</div>
</div>
</div>
<div className="flex items-center space-x-2">
{!selectedMessage.read && <Circle className="w-4 h-4 text-blue-400" />}
{selectedMessage.responded && <CheckCircle className="w-4 h-4 text-green-400" />}
</div>
</div>
{/* Message Body */}
<div className="p-4 bg-white/5 rounded-lg border border-white/10">
<h4 className="text-white font-medium mb-3">Message:</h4>
<div className="text-white/80 whitespace-pre-wrap leading-relaxed">
{selectedMessage.message}
</div>
</div>
{/* Actions */}
<div className="flex space-x-3">
<button
onClick={() => setShowReplyModal(true)}
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<Reply className="w-4 h-4" />
<span>Reply</span>
</button>
<button
onClick={() => setSelectedMessage(null)}
className="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors"
>
Close
</button>
</div>
</div>
) : (
<div className="text-center py-12 text-white/50">
<Eye className="w-12 h-12 mx-auto mb-4 opacity-50" />
<p>Select a message to view details</p>
</div>
)}
</div>
</div>
{/* Reply Modal */}
<AnimatePresence>
{showReplyModal && selectedMessage && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={() => setShowReplyModal(false)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-gray-900/95 backdrop-blur-xl border border-white/20 rounded-2xl p-6 max-w-2xl w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-white">Reply to {selectedMessage.name}</h2>
<button
onClick={() => setShowReplyModal(false)}
className="p-2 hover:bg-white/10 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-white/70" />
</button>
</div>
<div className="space-y-4">
<textarea
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
placeholder="Type your reply..."
className="w-full h-32 p-3 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
/>
<div className="flex space-x-3">
<button
onClick={handleReply}
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
<Send className="w-4 h-4" />
<span>Send Reply</span>
</button>
<button
onClick={() => setShowReplyModal(false)}
className="px-4 py-2 bg-white/10 text-white rounded-lg hover:bg-white/20 transition-colors"
>
Cancel
</button>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
>>>>>>> dev

733
components/GhostEditor.tsx Normal file
View File

@@ -0,0 +1,733 @@
'use client';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Save,
X,
Eye,
Settings,
Globe,
Github,
Image as ImageIcon,
Bold,
Italic,
List,
Quote,
Code,
Link2,
ListOrdered,
Underline,
Strikethrough,
Type,
Columns
} from 'lucide-react';
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;
}
interface GhostEditorProps {
isOpen: boolean;
onClose: () => void;
project?: Project | null;
onSave: (projectData: Partial<Project>) => void;
isCreating: boolean;
}
export const GhostEditor: React.FC<GhostEditorProps> = ({
isOpen,
onClose,
project,
onSave,
isCreating
}) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [content, setContent] = useState('');
const [category, setCategory] = useState('Web Development');
const [tags, setTags] = useState<string[]>([]);
const [github, setGithub] = useState('');
const [live, setLive] = useState('');
const [featured, setFeatured] = useState(false);
const [published, setPublished] = useState(false);
const [difficulty, setDifficulty] = useState('Intermediate');
// Editor UI state
const [viewMode, setViewMode] = useState<'edit' | 'preview' | 'split'>('split');
const [showSettings, setShowSettings] = useState(false);
const [wordCount, setWordCount] = useState(0);
const [readingTime, setReadingTime] = useState(0);
const titleRef = useRef<HTMLTextAreaElement>(null);
const contentRef = useRef<HTMLTextAreaElement>(null);
const previewRef = useRef<HTMLDivElement>(null);
const categories = ['Web Development', 'Full-Stack', 'Web Application', 'Mobile App', 'Design'];
const difficulties = ['Beginner', 'Intermediate', 'Advanced', 'Expert'];
useEffect(() => {
if (project && !isCreating) {
setTitle(project.title);
setDescription(project.description);
setContent(project.content || '');
setCategory(project.category);
setTags(project.tags || []);
setGithub(project.github || '');
setLive(project.live || '');
setFeatured(project.featured);
setPublished(project.published);
setDifficulty(project.difficulty || 'Intermediate');
} else {
// Reset for new project
setTitle('');
setDescription('');
setContent('');
setCategory('Web Development');
setTags([]);
setGithub('');
setLive('');
setFeatured(false);
setPublished(false);
setDifficulty('Intermediate');
}
}, [project, isCreating, isOpen]);
// Calculate word count and reading time
useEffect(() => {
const words = content.trim().split(/\s+/).filter(word => word.length > 0).length;
setWordCount(words);
setReadingTime(Math.ceil(words / 200)); // Average reading speed: 200 words/minute
}, [content]);
const handleSave = () => {
const projectData = {
title,
description,
content,
category,
tags,
github,
live,
featured,
published,
difficulty
};
onSave(projectData);
};
const addTag = (tag: string) => {
if (tag.trim() && !tags.includes(tag.trim())) {
setTags([...tags, tag.trim()]);
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter(tag => tag !== tagToRemove));
};
const insertMarkdown = useCallback((syntax: string, selectedText: string = '') => {
if (!contentRef.current) return;
const textarea = contentRef.current;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = selectedText || content.substring(start, end);
let newText = '';
let cursorOffset = 0;
switch (syntax) {
case 'bold':
newText = `**${selection || 'bold text'}**`;
cursorOffset = selection ? newText.length : 2;
break;
case 'italic':
newText = `*${selection || 'italic text'}*`;
cursorOffset = selection ? newText.length : 1;
break;
case 'underline':
newText = `<u>${selection || 'underlined text'}</u>`;
cursorOffset = selection ? newText.length : 3;
break;
case 'strikethrough':
newText = `~~${selection || 'strikethrough text'}~~`;
cursorOffset = selection ? newText.length : 2;
break;
case 'heading1':
newText = `# ${selection || 'Heading 1'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'heading2':
newText = `## ${selection || 'Heading 2'}`;
cursorOffset = selection ? newText.length : 3;
break;
case 'heading3':
newText = `### ${selection || 'Heading 3'}`;
cursorOffset = selection ? newText.length : 4;
break;
case 'list':
newText = `- ${selection || 'List item'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'list-ordered':
newText = `1. ${selection || 'List item'}`;
cursorOffset = selection ? newText.length : 3;
break;
case 'quote':
newText = `> ${selection || 'Quote'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'code':
if (selection.includes('\n')) {
newText = `\`\`\`\n${selection || 'code block'}\n\`\`\``;
cursorOffset = selection ? newText.length : 4;
} else {
newText = `\`${selection || 'code'}\``;
cursorOffset = selection ? newText.length : 1;
}
break;
case 'link':
newText = `[${selection || 'link text'}](url)`;
cursorOffset = selection ? newText.length - 4 : newText.length - 4;
break;
case 'image':
newText = `![${selection || 'alt text'}](image-url)`;
cursorOffset = selection ? newText.length - 11 : newText.length - 11;
break;
case 'divider':
newText = '\n---\n';
cursorOffset = newText.length;
break;
default:
return;
}
const newContent = content.substring(0, start) + newText + content.substring(end);
setContent(newContent);
// Focus and set cursor position
setTimeout(() => {
textarea.focus();
const newPosition = start + cursorOffset;
textarea.setSelectionRange(newPosition, newPosition);
}, 0);
}, [content]);
const autoResizeTextarea = (element: HTMLTextAreaElement) => {
element.style.height = 'auto';
element.style.height = element.scrollHeight + 'px';
};
// Render markdown preview
const renderMarkdownPreview = (markdown: string) => {
// Simple markdown renderer for preview
const html = markdown
// Headers
.replace(/^### (.*$)/gim, '<h3 class="text-xl font-semibold text-white mb-3 mt-6">$1</h3>')
.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-bold text-white mb-4 mt-8">$1</h2>')
.replace(/^# (.*$)/gim, '<h1 class="text-3xl font-bold text-white mb-6 mt-8">$1</h1>')
// Bold and Italic
.replace(/\*\*(.*?)\*\*/g, '<strong class="font-bold">$1</strong>')
.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>')
// Underline and Strikethrough
.replace(/<u>(.*?)<\/u>/g, '<u class="underline">$1</u>')
.replace(/~~(.*?)~~/g, '<del class="line-through opacity-75">$1</del>')
// Code
.replace(/```([^`]+)```/g, '<pre class="bg-gray-800 border border-gray-700 rounded-lg p-4 my-4 overflow-x-auto"><code class="text-green-400 font-mono text-sm">$1</code></pre>')
.replace(/`([^`]+)`/g, '<code class="bg-gray-800 border border-gray-700 rounded px-2 py-1 font-mono text-sm text-green-400">$1</code>')
// Lists
.replace(/^\- (.*$)/gim, '<li class="ml-4 mb-1">• $1</li>')
.replace(/^\d+\. (.*$)/gim, '<li class="ml-4 mb-1 list-decimal">$1</li>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-400 hover:text-blue-300 underline" target="_blank">$1</a>')
// Images
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="max-w-full h-auto rounded-lg my-4" />')
// Quotes
.replace(/^> (.*$)/gim, '<blockquote class="border-l-4 border-blue-500 pl-4 py-2 my-4 bg-gray-800/50 italic text-gray-300">$1</blockquote>')
// Dividers
.replace(/^---$/gim, '<hr class="border-gray-600 my-8" />')
// Paragraphs
.replace(/\n\n/g, '</p><p class="mb-4 text-gray-200 leading-relaxed">')
.replace(/\n/g, '<br />');
return `<div class="prose prose-invert max-w-none"><p class="mb-4 text-gray-200 leading-relaxed">${html}</p></div>`;
};
if (!isOpen) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/95 backdrop-blur-sm z-50"
>
{/* Professional Ghost Editor */}
<div className="h-full flex flex-col bg-gray-900">
{/* Top Navigation Bar */}
<div className="flex items-center justify-between p-4 border-b border-gray-700 bg-gray-800">
<div className="flex items-center space-x-4">
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium text-white">
{isCreating ? 'New Project' : 'Editing Project'}
</span>
</div>
<div className="flex items-center space-x-2">
{published ? (
<span className="px-3 py-1 bg-green-600 text-white rounded-full text-sm font-medium">
Published
</span>
) : (
<span className="px-3 py-1 bg-gray-600 text-gray-300 rounded-full text-sm font-medium">
Draft
</span>
)}
{featured && (
<span className="px-3 py-1 bg-purple-600 text-white rounded-full text-sm font-medium">
Featured
</span>
)}
</div>
</div>
{/* View Mode Toggle */}
<div className="flex items-center space-x-2">
<div className="flex items-center bg-gray-700 rounded-lg p-1">
<button
onClick={() => setViewMode('edit')}
className={`p-2 rounded transition-colors ${
viewMode === 'edit' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
}`}
title="Edit Mode"
>
<Type className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('split')}
className={`p-2 rounded transition-colors ${
viewMode === 'split' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
}`}
title="Split View"
>
<Columns className="w-4 h-4" />
</button>
<button
onClick={() => setViewMode('preview')}
className={`p-2 rounded transition-colors ${
viewMode === 'preview' ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white'
}`}
title="Preview Mode"
>
<Eye className="w-4 h-4" />
</button>
</div>
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-2 rounded-lg transition-colors ${
showSettings ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-gray-700'
}`}
>
<Settings className="w-5 h-5" />
</button>
<button
onClick={handleSave}
className="flex items-center space-x-2 px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
>
<Save className="w-4 h-4" />
<span>Save</span>
</button>
</div>
</div>
{/* Rich Text Toolbar */}
<div className="flex items-center justify-between p-3 border-b border-gray-700 bg-gray-800/50">
<div className="flex items-center space-x-1">
{/* Text Formatting */}
<div className="flex items-center space-x-1 pr-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('bold')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Bold (Ctrl+B)"
>
<Bold className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('italic')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Italic (Ctrl+I)"
>
<Italic className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('underline')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Underline"
>
<Underline className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('strikethrough')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Strikethrough"
>
<Strikethrough className="w-4 h-4" />
</button>
</div>
{/* Headers */}
<div className="flex items-center space-x-1 px-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('heading1')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 1"
>
H1
</button>
<button
onClick={() => insertMarkdown('heading2')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 2"
>
H2
</button>
<button
onClick={() => insertMarkdown('heading3')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 3"
>
H3
</button>
</div>
{/* Lists */}
<div className="flex items-center space-x-1 px-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('list')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Bullet List"
>
<List className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('list-ordered')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Numbered List"
>
<ListOrdered className="w-4 h-4" />
</button>
</div>
{/* Insert Elements */}
<div className="flex items-center space-x-1 px-2">
<button
onClick={() => insertMarkdown('link')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Insert Link"
>
<Link2 className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('image')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Insert Image"
>
<ImageIcon className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('code')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Code Block"
>
<Code className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('quote')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Quote"
>
<Quote className="w-4 h-4" />
</button>
</div>
</div>
{/* Stats */}
<div className="flex items-center space-x-4 text-sm text-gray-400">
<span>{wordCount} words</span>
<span>{readingTime} min read</span>
</div>
</div>
{/* Main Editor Area */}
<div className="flex-1 flex overflow-hidden">
{/* Content Area */}
<div className="flex-1 flex">
{/* Editor Pane */}
{(viewMode === 'edit' || viewMode === 'split') && (
<div className={`${viewMode === 'split' ? 'w-1/2' : 'w-full'} flex flex-col bg-gray-900`}>
{/* Title & Description */}
<div className="p-8 border-b border-gray-800">
<textarea
ref={titleRef}
value={title}
onChange={(e) => {
setTitle(e.target.value);
autoResizeTextarea(e.target);
}}
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
placeholder="Project title..."
className="w-full text-5xl font-bold text-white bg-transparent border-none outline-none placeholder-gray-500 resize-none overflow-hidden leading-tight mb-6"
rows={1}
/>
<textarea
value={description}
onChange={(e) => {
setDescription(e.target.value);
autoResizeTextarea(e.target);
}}
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
placeholder="Brief description of your project..."
className="w-full text-xl text-gray-300 bg-transparent border-none outline-none placeholder-gray-500 resize-none overflow-hidden leading-relaxed"
rows={1}
/>
</div>
{/* Content Editor */}
<div className="flex-1 p-8">
<textarea
ref={contentRef}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Start writing your story...
Use Markdown for formatting:
**Bold text** or *italic text*
# Large heading
## Medium heading
### Small heading
- Bullet points
1. Numbered lists
> Quotes
`code`
[Links](https://example.com)
![Images](image-url)"
className="w-full h-full text-lg text-white bg-transparent border-none outline-none placeholder-gray-600 resize-none font-mono leading-relaxed focus:ring-0"
style={{ minHeight: '500px' }}
/>
</div>
</div>
)}
{/* Preview Pane */}
{(viewMode === 'preview' || viewMode === 'split') && (
<div className={`${viewMode === 'split' ? 'w-1/2 border-l border-gray-700' : 'w-full'} bg-gray-850 overflow-y-auto`}>
<div className="p-8">
{/* Preview Header */}
<div className="mb-8 border-b border-gray-700 pb-8">
<h1 className="text-5xl font-bold text-white mb-6 leading-tight">
{title || 'Project title...'}
</h1>
<p className="text-xl text-gray-300 leading-relaxed">
{description || 'Brief description of your project...'}
</p>
</div>
{/* Preview Content */}
<div
ref={previewRef}
className="prose prose-invert max-w-none"
dangerouslySetInnerHTML={{
__html: content ? renderMarkdownPreview(content) : '<p class="text-gray-500 italic">Start writing to see the preview...</p>'
}}
/>
</div>
</div>
)}
</div>
{/* Settings Sidebar */}
<AnimatePresence>
{showSettings && (
<motion.div
initial={{ x: 320 }}
animate={{ x: 0 }}
exit={{ x: 320 }}
className="w-80 bg-gray-800 border-l border-gray-700 flex flex-col"
>
<div className="p-6 border-b border-gray-700">
<h3 className="text-lg font-semibold text-white">Project Settings</h3>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-8">
{/* Status */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Publication</h4>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-white">Published</span>
<button
onClick={() => setPublished(!published)}
className={`w-12 h-6 rounded-full transition-colors relative ${
published ? 'bg-green-600' : 'bg-gray-600'
}`}
>
<div className={`w-4 h-4 bg-white rounded-full transition-transform absolute top-1 ${
published ? 'translate-x-7' : 'translate-x-1'
}`} />
</button>
</div>
<div className="flex items-center justify-between">
<span className="text-white">Featured</span>
<button
onClick={() => setFeatured(!featured)}
className={`w-12 h-6 rounded-full transition-colors relative ${
featured ? 'bg-purple-600' : 'bg-gray-600'
}`}
>
<div className={`w-4 h-4 bg-white rounded-full transition-transform absolute top-1 ${
featured ? 'translate-x-7' : 'translate-x-1'
}`} />
</button>
</div>
</div>
</div>
{/* Category & Difficulty */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Classification</h4>
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">Category</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<div>
<label className="block text-gray-300 text-sm mb-2">Difficulty</label>
<select
value={difficulty}
onChange={(e) => setDifficulty(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{difficulties.map(diff => (
<option key={diff} value={diff}>{diff}</option>
))}
</select>
</div>
</div>
</div>
{/* Links */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">External Links</h4>
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">
<Github className="w-4 h-4 inline mr-1" />
GitHub Repository
</label>
<input
type="url"
value={github}
onChange={(e) => setGithub(e.target.value)}
placeholder="https://github.com/..."
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-gray-300 text-sm mb-2">
<Globe className="w-4 h-4 inline mr-1" />
Live Demo
</label>
<input
type="url"
value={live}
onChange={(e) => setLive(e.target.value)}
placeholder="https://..."
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Tags */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Tags</h4>
<div className="space-y-3">
<input
type="text"
placeholder="Add a tag and press Enter"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTag(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center space-x-1 px-3 py-1 bg-blue-600 text-white rounded-full text-sm"
>
<span>{tag}</span>
<button
onClick={() => removeTag(tag)}
className="text-blue-200 hover:text-white"
>
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</motion.div>
</AnimatePresence>
);
};

View File

@@ -99,23 +99,23 @@ export default function ImportExport() {
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center">
<FileText className="w-5 h-5 mr-2" />
<div className="admin-glass-card rounded-lg p-6">
<h3 className="text-lg font-semibold text-white mb-4 flex items-center">
<FileText className="w-5 h-5 mr-2 text-blue-400" />
Import & Export
</h3>
<div className="space-y-4">
{/* Export Section */}
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Export Projekte</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
<div className="admin-glass-light rounded-lg p-4">
<h4 className="font-medium text-white mb-2">Export Projekte</h4>
<p className="text-sm text-white/70 mb-3">
Alle Projekte als JSON-Datei herunterladen
</p>
<button
onClick={handleExport}
disabled={isExporting}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 hover:scale-105 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<Download className="w-4 h-4 mr-2" />
{isExporting ? 'Exportiere...' : 'Exportieren'}
@@ -123,12 +123,12 @@ export default function ImportExport() {
</div>
{/* Import Section */}
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-2">Import Projekte</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
<div className="admin-glass-light rounded-lg p-4">
<h4 className="font-medium text-white mb-2">Import Projekte</h4>
<p className="text-sm text-white/70 mb-3">
JSON-Datei mit Projekten hochladen
</p>
<label className="flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 cursor-pointer">
<label className="flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 hover:scale-105 transition-all cursor-pointer">
<Upload className="w-4 h-4 mr-2" />
{isImporting ? 'Importiere...' : 'Datei auswählen'}
<input
@@ -143,16 +143,16 @@ export default function ImportExport() {
{/* Import Results */}
{importResult && (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 className="font-medium text-gray-900 dark:text-white mb-2 flex items-center">
<div className="admin-glass-light rounded-lg p-4">
<h4 className="font-medium text-white mb-2 flex items-center">
{importResult.success ? (
<CheckCircle className="w-5 h-5 mr-2 text-green-500" />
<CheckCircle className="w-5 h-5 mr-2 text-green-400" />
) : (
<AlertCircle className="w-5 h-5 mr-2 text-red-500" />
<AlertCircle className="w-5 h-5 mr-2 text-red-400" />
)}
Import Ergebnis
</h4>
<div className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<div className="text-sm text-white/70 space-y-1">
<p><strong>Importiert:</strong> {importResult.results.imported}</p>
<p><strong>Übersprungen:</strong> {importResult.results.skipped}</p>
{importResult.results.errors.length > 0 && (
@@ -160,7 +160,7 @@ export default function ImportExport() {
<p><strong>Fehler:</strong></p>
<ul className="list-disc list-inside ml-4">
{importResult.results.errors.map((error, index) => (
<li key={index} className="text-red-500">{error}</li>
<li key={index} className="text-red-400">{error}</li>
))}
</ul>
</div>

View File

@@ -1,5 +1,6 @@
'use client';
<<<<<<< HEAD
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
@@ -15,11 +16,29 @@ import {
Edit,
Trash2,
Eye
=======
import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Mail,
Settings,
TrendingUp,
Plus,
Shield,
Users,
Activity,
Database,
Home,
LogOut,
Menu,
X
>>>>>>> dev
} from 'lucide-react';
import Link from 'next/link';
import { EmailManager } from './EmailManager';
import { AnalyticsDashboard } from './AnalyticsDashboard';
import ImportExport from './ImportExport';
<<<<<<< HEAD
interface Project {
id: number;
@@ -54,10 +73,31 @@ interface Project {
loadTime: string;
};
analytics: {
=======
import { ProjectManager } from './ProjectManager';
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?: {
>>>>>>> dev
views: number;
likes: number;
shares: number;
};
<<<<<<< HEAD
}
const ModernAdminDashboard: React.FC = () => {
@@ -242,11 +282,363 @@ const ModernAdminDashboard: React.FC = () => {
</div>
<div className="w-12 h-12 bg-orange-500/20 rounded-xl flex items-center justify-center">
<Zap className="w-6 h-6 text-orange-400" />
=======
performance?: {
lighthouse: number;
};
}
interface ModernAdminDashboardProps {
isAuthenticated?: boolean;
}
const ModernAdminDashboard: React.FC<ModernAdminDashboardProps> = ({ isAuthenticated = true }) => {
const [activeTab, setActiveTab] = useState<'overview' | 'projects' | 'emails' | 'analytics' | 'settings'>('overview');
const [projects, setProjects] = useState<Project[]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isLoading, setIsLoading] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [analytics, setAnalytics] = useState<Record<string, unknown> | null>(null);
const [emails, setEmails] = useState<Record<string, unknown>[]>([]);
const [systemStats, setSystemStats] = useState<Record<string, unknown> | null>(null);
const loadProjects = useCallback(async () => {
if (!isAuthenticated) return;
try {
setIsLoading(true);
const response = await fetch('/api/projects', {
headers: {
'x-admin-request': 'true'
}
});
if (!response.ok) {
console.warn('Failed to load projects:', response.status);
setProjects([]);
return;
}
const data = await response.json();
setProjects(data.projects || []);
} catch {
setProjects([]);
} finally {
setIsLoading(false);
}
}, [isAuthenticated]);
const loadAnalytics = useCallback(async () => {
if (!isAuthenticated) return;
try {
const response = await fetch('/api/analytics/dashboard', {
headers: {
'x-admin-request': 'true'
}
});
if (response.ok) {
const data = await response.json();
setAnalytics(data);
}
} catch (error) {
console.error('Error loading analytics:', error);
}
}, [isAuthenticated]);
const loadEmails = useCallback(async () => {
if (!isAuthenticated) return;
try {
const response = await fetch('/api/contacts', {
headers: {
'x-admin-request': 'true'
}
});
if (response.ok) {
const data = await response.json();
setEmails(data.contacts || []);
}
} catch (error) {
console.error('Error loading emails:', error);
}
}, [isAuthenticated]);
const loadSystemStats = useCallback(async () => {
if (!isAuthenticated) return;
try {
const response = await fetch('/api/health', {
headers: {
'x-admin-request': 'true'
}
});
if (response.ok) {
const data = await response.json();
setSystemStats(data);
}
} catch (error) {
console.error('Error loading system stats:', error);
}
}, [isAuthenticated]);
const loadAllData = useCallback(async () => {
await Promise.all([
loadProjects(),
loadAnalytics(),
loadEmails(),
loadSystemStats()
]);
}, [loadProjects, loadAnalytics, loadEmails, loadSystemStats]);
// Real stats from API data
const stats = {
totalProjects: projects.length,
publishedProjects: projects.filter(p => p.published).length,
totalViews: (analytics?.totalViews as number) || projects.reduce((sum, p) => sum + (p.analytics?.views || 0), 0),
unreadEmails: emails.filter(e => !(e.read as boolean)).length,
avgPerformance: (analytics?.avgPerformance as number) || (projects.length > 0 ?
Math.round(projects.reduce((sum, p) => sum + (p.performance?.lighthouse || 90), 0) / projects.length) : 90),
systemHealth: (systemStats?.status as string) || 'unknown',
totalUsers: (analytics?.totalUsers as number) || 0,
bounceRate: (analytics?.bounceRate as number) || 0,
avgSessionDuration: (analytics?.avgSessionDuration as number) || 0
};
useEffect(() => {
// Load all data if authenticated
if (isAuthenticated) {
loadAllData();
}
}, [isAuthenticated, loadAllData]);
const navigation = [
{ id: 'overview', label: 'Dashboard', icon: Home, color: 'blue', description: 'Overview & Statistics' },
{ id: 'projects', label: 'Projects', icon: Database, color: 'green', description: 'Manage Projects' },
{ id: 'emails', label: 'Emails', icon: Mail, color: 'purple', description: 'Email Management' },
{ id: 'analytics', label: 'Analytics', icon: Activity, color: 'orange', description: 'Site Analytics' },
{ id: 'settings', label: 'Settings', icon: Settings, color: 'gray', description: 'System Settings' }
];
return (
<div className="min-h-screen">
{/* Animated Background - same as main site */}
<div className="fixed inset-0 animated-bg"></div>
{/* Admin Navbar - Horizontal Navigation */}
<div className="relative z-10">
<div className="admin-glass border-b border-white/20 sticky top-0">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Left side - Logo and Admin Panel */}
<div className="flex items-center space-x-4">
<Link
href="/"
className="flex items-center space-x-2 text-white/90 hover:text-white transition-colors"
>
<Home size={20} className="text-blue-400" />
<span className="font-medium text-white">Portfolio</span>
</Link>
<div className="h-6 w-px bg-white/30" />
<div className="flex items-center space-x-2">
<Shield size={20} className="text-purple-400" />
<span className="text-white font-semibold">Admin Panel</span>
</div>
</div>
{/* Center - Desktop Navigation */}
<div className="hidden md:flex items-center space-x-1">
{navigation.map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id as 'overview' | 'projects' | 'emails' | 'analytics' | 'settings')}
className={`flex items-center space-x-2 px-4 py-2 rounded-lg transition-all duration-200 ${
activeTab === item.id
? 'admin-glass-light border border-blue-500/40 text-blue-300 shadow-lg'
: 'text-white/80 hover:text-white hover:admin-glass-light'
}`}
>
<item.icon size={16} className={activeTab === item.id ? 'text-blue-400' : 'text-white/70'} />
<span className="font-medium text-sm">{item.label}</span>
</button>
))}
</div>
{/* Right side - User info and Logout */}
<div className="flex items-center space-x-4">
<div className="hidden sm:block text-sm text-white/80">
Welcome, <span className="text-white font-semibold">Dennis</span>
</div>
<button
onClick={() => window.location.href = '/api/auth/logout'}
className="flex items-center space-x-2 px-3 py-2 rounded-lg admin-glass-light hover:bg-red-500/20 text-red-300 hover:text-red-200 transition-all duration-200"
>
<LogOut size={16} />
<span className="hidden sm:inline text-sm font-medium">Logout</span>
</button>
{/* Mobile menu button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden flex items-center justify-center p-2 rounded-lg admin-glass-light text-white hover:text-blue-300 transition-colors"
>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
</div>
</div>
{/* Mobile Navigation Menu */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden border-t border-white/20 admin-glass-light"
>
<div className="px-4 py-4 space-y-2">
{navigation.map((item) => (
<button
key={item.id}
onClick={() => {
setActiveTab(item.id as 'overview' | 'projects' | 'emails' | 'analytics' | 'settings');
setMobileMenuOpen(false);
}}
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-200 ${
activeTab === item.id
? 'admin-glass-light border border-blue-500/40 text-blue-300 shadow-lg'
: 'text-white/80 hover:text-white hover:admin-glass-light'
}`}
>
<item.icon size={18} className={activeTab === item.id ? 'text-blue-400' : 'text-white/70'} />
<div className="text-left">
<div className="font-medium text-sm">{item.label}</div>
<div className="text-xs opacity-70">{item.description}</div>
</div>
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Main Content - Full Width Horizontal Layout */}
<div className="px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-6 lg:py-8">
{/* Content */}
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
{activeTab === 'overview' && (
<div className="space-y-8">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold text-white">Admin Dashboard</h1>
<p className="text-white/80 text-lg">Manage your portfolio and monitor performance</p>
</div>
</div>
{/* Stats Grid - Mobile: 2x3, Desktop: 6x1 horizontal */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-3 md:gap-6">
<div
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
onClick={() => setActiveTab('projects')}
>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<p className="text-white/80 text-xs md:text-sm font-medium">Projects</p>
<Database size={20} className="text-blue-400" />
</div>
<p className="text-xl md:text-2xl font-bold text-white">{stats.totalProjects}</p>
<p className="text-green-400 text-xs font-medium">{stats.publishedProjects} published</p>
</div>
</div>
<div
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
onClick={() => setActiveTab('analytics')}
>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<p className="text-white/80 text-xs md:text-sm font-medium">Page Views</p>
<Activity size={20} className="text-purple-400" />
</div>
<p className="text-xl md:text-2xl font-bold text-white">{stats.totalViews.toLocaleString()}</p>
<p className="text-blue-400 text-xs font-medium">{stats.totalUsers} users</p>
</div>
</div>
<div
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
onClick={() => setActiveTab('emails')}
>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<p className="text-white/80 text-xs md:text-sm font-medium">Messages</p>
<Mail size={20} className="text-green-400" />
</div>
<p className="text-xl md:text-2xl font-bold text-white">{emails.length}</p>
<p className="text-red-400 text-xs font-medium">{stats.unreadEmails} unread</p>
</div>
</div>
<div
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
onClick={() => setActiveTab('analytics')}
>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<p className="text-white/80 text-xs md:text-sm font-medium">Performance</p>
<TrendingUp size={20} className="text-orange-400" />
</div>
<p className="text-xl md:text-2xl font-bold text-white">{stats.avgPerformance}</p>
<p className="text-orange-400 text-xs font-medium">Lighthouse Score</p>
</div>
</div>
<div
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
onClick={() => setActiveTab('analytics')}
>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<p className="text-white/80 text-xs md:text-sm font-medium">Bounce Rate</p>
<Users size={20} className="text-red-400" />
</div>
<p className="text-xl md:text-2xl font-bold text-white">{stats.bounceRate}%</p>
<p className="text-red-400 text-xs font-medium">Exit rate</p>
</div>
</div>
<div
className="admin-glass-light p-4 rounded-xl hover:scale-105 transition-all duration-200 cursor-pointer"
onClick={() => setActiveTab('settings')}
>
<div className="flex flex-col space-y-2">
<div className="flex items-center justify-between">
<p className="text-white/80 text-xs md:text-sm font-medium">System</p>
<Shield size={20} className="text-green-400" />
</div>
<p className="text-xl md:text-2xl font-bold text-white">Online</p>
<div className="flex items-center space-x-1">
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div>
<p className="text-green-400 text-xs font-medium">All systems operational</p>
>>>>>>> dev
</div>
</div>
</div>
</div>
<<<<<<< HEAD
{/* Recent Projects */}
<div className="bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 p-6">
<div className="flex items-center justify-between mb-6">
@@ -410,10 +802,216 @@ const ModernAdminDashboard: React.FC = () => {
)}
</AnimatePresence>
</div>
=======
{/* Recent Activity & Quick Actions - Mobile: vertical, Desktop: horizontal */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Recent Activity */}
<div className="admin-glass-card p-6 rounded-xl md:col-span-2">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-white">Recent Activity</h2>
<button
onClick={() => loadAllData()}
className="text-blue-400 hover:text-blue-300 text-sm font-medium px-3 py-1 admin-glass-light rounded-lg transition-colors"
>
Refresh
</button>
</div>
{/* Mobile: vertical stack, Desktop: horizontal columns */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-6">
<h3 className="text-sm font-medium text-white/60 uppercase tracking-wider">Projects</h3>
<div className="space-y-4">
{projects.slice(0, 3).map((project) => (
<div key={project.id} className="flex items-start space-x-3 p-4 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('projects')}>
<div className="flex-1 min-w-0">
<p className="text-white font-medium text-sm truncate">{project.title}</p>
<p className="text-white/60 text-xs">{project.published ? 'Published' : 'Draft'} {project.analytics?.views || 0} views</p>
<div className="flex items-center space-x-2 mt-2">
<span className={`px-2 py-1 rounded-full text-xs ${project.published ? 'bg-green-500/20 text-green-400' : 'bg-yellow-500/20 text-yellow-400'}`}>
{project.published ? 'Live' : 'Draft'}
</span>
{project.featured && (
<span className="px-2 py-1 bg-purple-500/20 text-purple-400 rounded-full text-xs">Featured</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-white/60 uppercase tracking-wider">Messages</h3>
<div className="space-y-3">
{emails.slice(0, 3).map((email, index) => (
<div key={index} className="flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 cursor-pointer" onClick={() => setActiveTab('emails')}>
<div className="w-8 h-8 bg-green-500/30 rounded-lg flex items-center justify-center flex-shrink-0">
<Mail size={14} className="text-green-400" />
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-medium text-sm truncate">From {email.name as string}</p>
<p className="text-white/60 text-xs truncate">{(email.subject as string) || 'No subject'}</p>
</div>
{!(email.read as boolean) && (
<div className="w-2 h-2 bg-red-400 rounded-full flex-shrink-0"></div>
)}
</div>
))}
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="admin-glass-card p-6 rounded-xl">
<h2 className="text-xl font-bold text-white mb-6">Quick Actions</h2>
<div className="space-y-4">
<button
onClick={() => window.location.href = '/editor'}
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
>
<div className="w-10 h-10 bg-green-500/30 rounded-lg flex items-center justify-center group-hover:bg-green-500/40 transition-colors">
<Plus size={18} className="text-green-400" />
</div>
<div>
<p className="text-white font-medium text-sm">Ghost Editor</p>
<p className="text-white/60 text-xs">Professional writing tool</p>
</div>
</button>
<button
onClick={() => setActiveTab('analytics')}
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
>
<div className="w-10 h-10 bg-red-500/30 rounded-lg flex items-center justify-center group-hover:bg-red-500/40 transition-colors">
<Activity size={18} className="text-red-400" />
</div>
<div>
<p className="text-white font-medium text-sm">Reset Analytics</p>
<p className="text-white/60 text-xs">Clear analytics data</p>
</div>
</button>
<button
onClick={() => setActiveTab('emails')}
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
>
<div className="w-10 h-10 bg-green-500/30 rounded-lg flex items-center justify-center group-hover:bg-green-500/40 transition-colors">
<Mail size={18} className="text-green-400" />
</div>
<div>
<p className="text-white font-medium text-sm">View Messages</p>
<p className="text-white/60 text-xs">{stats.unreadEmails} unread messages</p>
</div>
</button>
<button
onClick={() => setActiveTab('analytics')}
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
>
<div className="w-10 h-10 bg-purple-500/30 rounded-lg flex items-center justify-center group-hover:bg-purple-500/40 transition-colors">
<TrendingUp size={18} className="text-purple-400" />
</div>
<div>
<p className="text-white font-medium text-sm">Analytics</p>
<p className="text-white/60 text-xs">View detailed statistics</p>
</div>
</button>
<button
onClick={() => setActiveTab('settings')}
className="w-full flex items-center space-x-3 p-3 admin-glass-light rounded-lg hover:scale-[1.02] transition-all duration-200 text-left group"
>
<div className="w-10 h-10 bg-gray-500/30 rounded-lg flex items-center justify-center group-hover:bg-gray-500/40 transition-colors">
<Settings size={18} className="text-gray-400" />
</div>
<div>
<p className="text-white font-medium text-sm">Settings</p>
<p className="text-white/60 text-xs">System configuration</p>
</div>
</button>
</div>
</div>
</div>
</div>
)}
{activeTab === 'projects' && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-white">Project Management</h2>
<p className="text-white/70 mt-1">Manage your portfolio projects</p>
</div>
</div>
<ProjectManager projects={projects} onProjectsChange={loadProjects} />
</div>
)}
{activeTab === 'emails' && (
<EmailManager />
)}
{activeTab === 'analytics' && (
<AnalyticsDashboard isAuthenticated={isAuthenticated} />
)}
{activeTab === 'settings' && (
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold text-white">System Settings</h1>
<p className="text-white/60">Manage system configuration and preferences</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="admin-glass-card p-6 rounded-xl">
<h2 className="text-xl font-bold text-white mb-4">Import / Export</h2>
<p className="text-white/70 mb-4">Backup and restore your portfolio data</p>
<ImportExport />
</div>
<div className="admin-glass-card p-6 rounded-xl">
<h2 className="text-xl font-bold text-white mb-4">System Status</h2>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span className="text-white/80">Database</span>
<div className="flex items-center space-x-3">
<div className="w-4 h-4 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-green-400 font-medium">Online</span>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span className="text-white/80">Redis Cache</span>
<div className="flex items-center space-x-3">
<div className="w-4 h-4 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-green-400 font-medium">Online</span>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<span className="text-white/80">API Services</span>
<div className="flex items-center space-x-3">
<div className="w-4 h-4 bg-green-400 rounded-full animate-pulse"></div>
<span className="text-green-400 font-medium">Online</span>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</motion.div>
</AnimatePresence>
>>>>>>> dev
</div>
</div>
</div>
);
};
<<<<<<< HEAD
export default ModernAdminDashboard;
=======
export default ModernAdminDashboard;
>>>>>>> dev

View File

@@ -0,0 +1,348 @@
'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>
);
};

View File

@@ -0,0 +1,750 @@
'use client';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Save,
X,
Eye,
EyeOff,
Settings,
Globe,
Github,
Bold,
Italic,
List,
Quote,
Code,
Link2,
ListOrdered,
Underline,
Strikethrough,
GripVertical,
Image as ImageIcon
} from 'lucide-react';
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;
}
interface ResizableGhostEditorProps {
project?: Project | null;
onSave: (projectData: Partial<Project>) => void;
onClose: () => void;
isCreating: boolean;
}
export const ResizableGhostEditor: React.FC<ResizableGhostEditorProps> = ({
project,
onSave,
onClose,
isCreating
}) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [content, setContent] = useState('');
const [category, setCategory] = useState('Web Development');
const [tags, setTags] = useState<string[]>([]);
const [github, setGithub] = useState('');
const [live, setLive] = useState('');
const [featured, setFeatured] = useState(false);
const [published, setPublished] = useState(false);
const [difficulty, setDifficulty] = useState('Intermediate');
// Editor UI state
const [showPreview, setShowPreview] = useState(true);
const [showSettings, setShowSettings] = useState(false);
const [previewWidth, setPreviewWidth] = useState(50); // Percentage
const [wordCount, setWordCount] = useState(0);
const [readingTime, setReadingTime] = useState(0);
const [isResizing, setIsResizing] = useState(false);
const titleRef = useRef<HTMLTextAreaElement>(null);
const contentRef = useRef<HTMLTextAreaElement>(null);
const previewRef = useRef<HTMLDivElement>(null);
const resizeRef = useRef<HTMLDivElement>(null);
const categories = ['Web Development', 'Full-Stack', 'Web Application', 'Mobile App', 'Design'];
const difficulties = ['Beginner', 'Intermediate', 'Advanced', 'Expert'];
useEffect(() => {
if (project && !isCreating) {
setTitle(project.title);
setDescription(project.description);
setContent(project.content || '');
setCategory(project.category);
setTags(project.tags || []);
setGithub(project.github || '');
setLive(project.live || '');
setFeatured(project.featured);
setPublished(project.published);
setDifficulty(project.difficulty || 'Intermediate');
} else {
// Reset for new project
setTitle('');
setDescription('');
setContent('');
setCategory('Web Development');
setTags([]);
setGithub('');
setLive('');
setFeatured(false);
setPublished(false);
setDifficulty('Intermediate');
}
}, [project, isCreating]);
// Calculate word count and reading time
useEffect(() => {
const words = content.trim().split(/\s+/).filter(word => word.length > 0).length;
setWordCount(words);
setReadingTime(Math.ceil(words / 200)); // Average reading speed: 200 words/minute
}, [content]);
// Handle resizing
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing) return;
const containerWidth = window.innerWidth - (showSettings ? 320 : 0); // Account for settings sidebar
const newWidth = Math.max(20, Math.min(80, (e.clientX / containerWidth) * 100));
setPreviewWidth(100 - newWidth); // Invert since we're setting editor width
};
const handleMouseUp = () => {
setIsResizing(false);
};
if (isResizing) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [isResizing, showSettings]);
const handleSave = () => {
const projectData = {
title,
description,
content,
category,
tags,
github,
live,
featured,
published,
difficulty
};
onSave(projectData);
};
const addTag = (tag: string) => {
if (tag.trim() && !tags.includes(tag.trim())) {
setTags([...tags, tag.trim()]);
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter(tag => tag !== tagToRemove));
};
const insertMarkdown = useCallback((syntax: string, selectedText: string = '') => {
if (!contentRef.current) return;
const textarea = contentRef.current;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selection = selectedText || content.substring(start, end);
let newText = '';
let cursorOffset = 0;
switch (syntax) {
case 'bold':
newText = `**${selection || 'bold text'}**`;
cursorOffset = selection ? newText.length : 2;
break;
case 'italic':
newText = `*${selection || 'italic text'}*`;
cursorOffset = selection ? newText.length : 1;
break;
case 'underline':
newText = `<u>${selection || 'underlined text'}</u>`;
cursorOffset = selection ? newText.length : 3;
break;
case 'strikethrough':
newText = `~~${selection || 'strikethrough text'}~~`;
cursorOffset = selection ? newText.length : 2;
break;
case 'heading1':
newText = `# ${selection || 'Heading 1'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'heading2':
newText = `## ${selection || 'Heading 2'}`;
cursorOffset = selection ? newText.length : 3;
break;
case 'heading3':
newText = `### ${selection || 'Heading 3'}`;
cursorOffset = selection ? newText.length : 4;
break;
case 'list':
newText = `- ${selection || 'List item'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'list-ordered':
newText = `1. ${selection || 'List item'}`;
cursorOffset = selection ? newText.length : 3;
break;
case 'quote':
newText = `> ${selection || 'Quote'}`;
cursorOffset = selection ? newText.length : 2;
break;
case 'code':
if (selection.includes('\n')) {
newText = `\`\`\`\n${selection || 'code block'}\n\`\`\``;
cursorOffset = selection ? newText.length : 4;
} else {
newText = `\`${selection || 'code'}\``;
cursorOffset = selection ? newText.length : 1;
}
break;
case 'link':
newText = `[${selection || 'link text'}](url)`;
cursorOffset = selection ? newText.length - 4 : newText.length - 4;
break;
case 'image':
newText = `![${selection || 'alt text'}](image-url)`;
cursorOffset = selection ? newText.length - 11 : newText.length - 11;
break;
case 'divider':
newText = '\n---\n';
cursorOffset = newText.length;
break;
default:
return;
}
const newContent = content.substring(0, start) + newText + content.substring(end);
setContent(newContent);
// Focus and set cursor position
setTimeout(() => {
textarea.focus();
const newPosition = start + cursorOffset;
textarea.setSelectionRange(newPosition, newPosition);
}, 0);
}, [content]);
const autoResizeTextarea = (element: HTMLTextAreaElement) => {
element.style.height = 'auto';
element.style.height = element.scrollHeight + 'px';
};
// Enhanced markdown renderer with proper white text
const renderMarkdownPreview = (markdown: string) => {
const html = markdown
// Headers - WHITE TEXT
.replace(/^### (.*$)/gim, '<h3 class="text-xl font-semibold text-white mb-3 mt-6">$1</h3>')
.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-bold text-white mb-4 mt-8">$1</h2>')
.replace(/^# (.*$)/gim, '<h1 class="text-3xl font-bold text-white mb-6 mt-8">$1</h1>')
// Bold and Italic - WHITE TEXT
.replace(/\*\*(.*?)\*\*/g, '<strong class="font-bold text-white">$1</strong>')
.replace(/\*(.*?)\*/g, '<em class="italic text-white">$1</em>')
// Underline and Strikethrough - WHITE TEXT
.replace(/<u>(.*?)<\/u>/g, '<u class="underline text-white">$1</u>')
.replace(/~~(.*?)~~/g, '<del class="line-through opacity-75 text-white">$1</del>')
// Code
.replace(/```([^`]+)```/g, '<pre class="bg-gray-800 border border-gray-700 rounded-lg p-4 my-4 overflow-x-auto"><code class="text-green-400 font-mono text-sm">$1</code></pre>')
.replace(/`([^`]+)`/g, '<code class="bg-gray-800 border border-gray-700 rounded px-2 py-1 font-mono text-sm text-green-400">$1</code>')
// Lists - WHITE TEXT
.replace(/^\- (.*$)/gim, '<li class="ml-4 mb-1 text-white">• $1</li>')
.replace(/^\d+\. (.*$)/gim, '<li class="ml-4 mb-1 list-decimal text-white">$1</li>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-400 hover:text-blue-300 underline" target="_blank">$1</a>')
// Images
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" class="max-w-full h-auto rounded-lg my-4" />')
// Quotes - WHITE TEXT
.replace(/^> (.*$)/gim, '<blockquote class="border-l-4 border-blue-500 pl-4 py-2 my-4 bg-gray-800/50 italic text-gray-300">$1</blockquote>')
// Dividers
.replace(/^---$/gim, '<hr class="border-gray-600 my-8" />')
// Paragraphs - WHITE TEXT
.replace(/\n\n/g, '</p><p class="mb-4 text-white leading-relaxed">')
.replace(/\n/g, '<br />');
return `<div class="prose prose-invert max-w-none text-white"><p class="mb-4 text-white leading-relaxed">${html}</p></div>`;
};
return (
<div className="min-h-screen animated-bg">
{/* Professional Ghost Editor */}
<div className="h-screen flex flex-col bg-gray-900/80 backdrop-blur-sm">
{/* Top Navigation Bar */}
<div className="flex items-center justify-between p-4 border-b border-gray-700 admin-glass-card">
<div className="flex items-center space-x-4">
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium text-white">
{isCreating ? 'New Project' : 'Editing Project'}
</span>
</div>
<div className="flex items-center space-x-2">
{published ? (
<span className="px-3 py-1 bg-green-600 text-white rounded-full text-sm font-medium">
Published
</span>
) : (
<span className="px-3 py-1 bg-gray-600 text-gray-300 rounded-full text-sm font-medium">
Draft
</span>
)}
{featured && (
<span className="px-3 py-1 bg-purple-600 text-white rounded-full text-sm font-medium">
Featured
</span>
)}
</div>
</div>
{/* Controls */}
<div className="flex items-center space-x-2">
{/* Preview Toggle */}
<button
onClick={() => setShowPreview(!showPreview)}
className={`p-2 rounded transition-colors ${
showPreview ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-gray-700'
}`}
title="Toggle Preview"
>
{showPreview ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
<button
onClick={() => setShowSettings(!showSettings)}
className={`p-2 rounded-lg transition-colors ${
showSettings ? 'bg-blue-600 text-white' : 'text-gray-400 hover:text-white hover:bg-gray-700'
}`}
>
<Settings className="w-5 h-5" />
</button>
<button
onClick={handleSave}
className="flex items-center space-x-2 px-6 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
>
<Save className="w-4 h-4" />
<span>Save</span>
</button>
</div>
</div>
{/* Rich Text Toolbar */}
<div className="flex items-center justify-between p-3 border-b border-gray-700 admin-glass-light">
<div className="flex items-center space-x-1">
{/* Text Formatting */}
<div className="flex items-center space-x-1 pr-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('bold')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Bold (Ctrl+B)"
>
<Bold className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('italic')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Italic (Ctrl+I)"
>
<Italic className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('underline')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Underline"
>
<Underline className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('strikethrough')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Strikethrough"
>
<Strikethrough className="w-4 h-4" />
</button>
</div>
{/* Headers */}
<div className="flex items-center space-x-1 px-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('heading1')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 1"
>
H1
</button>
<button
onClick={() => insertMarkdown('heading2')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 2"
>
H2
</button>
<button
onClick={() => insertMarkdown('heading3')}
className="px-2 py-1 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors text-sm font-bold"
title="Heading 3"
>
H3
</button>
</div>
{/* Lists */}
<div className="flex items-center space-x-1 px-2 border-r border-gray-600">
<button
onClick={() => insertMarkdown('list')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Bullet List"
>
<List className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('list-ordered')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Numbered List"
>
<ListOrdered className="w-4 h-4" />
</button>
</div>
{/* Insert Elements */}
<div className="flex items-center space-x-1 px-2">
<button
onClick={() => insertMarkdown('link')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Insert Link"
>
<Link2 className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('image')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Insert Image"
>
<ImageIcon className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('code')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Code Block"
>
<Code className="w-4 h-4" />
</button>
<button
onClick={() => insertMarkdown('quote')}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded transition-colors"
title="Quote"
>
<Quote className="w-4 h-4" />
</button>
</div>
</div>
{/* Stats */}
<div className="flex items-center space-x-4 text-sm text-gray-400">
<span>{wordCount} words</span>
<span>{readingTime} min read</span>
{showPreview && (
<span>Preview: {previewWidth}%</span>
)}
</div>
</div>
{/* Main Editor Area */}
<div className="flex-1 flex overflow-hidden">
{/* Content Area */}
<div className="flex-1 flex">
{/* Editor Pane */}
<div
className={`flex flex-col bg-gray-900/90 transition-all duration-300 ${
showPreview ? `w-[${100 - previewWidth}%]` : 'w-full'
}`}
style={{ width: showPreview ? `${100 - previewWidth}%` : '100%' }}
>
{/* Title & Description */}
<div className="p-8 border-b border-gray-800">
<textarea
ref={titleRef}
value={title}
onChange={(e) => {
setTitle(e.target.value);
autoResizeTextarea(e.target);
}}
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
placeholder="Project title..."
className="w-full text-5xl font-bold text-white bg-transparent border-none outline-none placeholder-gray-500 resize-none overflow-hidden leading-tight mb-6"
rows={1}
/>
<textarea
value={description}
onChange={(e) => {
setDescription(e.target.value);
autoResizeTextarea(e.target);
}}
onInput={(e) => autoResizeTextarea(e.target as HTMLTextAreaElement)}
placeholder="Brief description of your project..."
className="w-full text-xl text-gray-300 bg-transparent border-none outline-none placeholder-gray-500 resize-none overflow-hidden leading-relaxed"
rows={1}
/>
</div>
{/* Content Editor */}
<div className="flex-1 p-8">
<textarea
ref={contentRef}
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Start writing your story...
Use Markdown for formatting:
**Bold text** or *italic text*
# Large heading
## Medium heading
### Small heading
- Bullet points
1. Numbered lists
> Quotes
`code`
[Links](https://example.com)
![Images](image-url)"
className="w-full h-full text-lg text-white bg-transparent border-none outline-none placeholder-gray-600 resize-none font-mono leading-relaxed focus:ring-0"
style={{ minHeight: '500px' }}
/>
</div>
</div>
{/* Resize Handle */}
{showPreview && (
<div
ref={resizeRef}
className="w-1 bg-gray-700 hover:bg-blue-500 cursor-col-resize flex items-center justify-center transition-colors group"
onMouseDown={() => setIsResizing(true)}
>
<GripVertical className="w-4 h-4 text-gray-600 group-hover:text-blue-400 transition-colors" />
</div>
)}
{/* Preview Pane */}
{showPreview && (
<div
className={`bg-gray-850 overflow-y-auto transition-all duration-300`}
style={{ width: `${previewWidth}%` }}
>
<div className="p-8">
{/* Preview Header */}
<div className="mb-8 border-b border-gray-700 pb-8">
<h1 className="text-5xl font-bold text-white mb-6 leading-tight">
{title || 'Project title...'}
</h1>
<p className="text-xl text-gray-300 leading-relaxed">
{description || 'Brief description of your project...'}
</p>
</div>
{/* Preview Content */}
<div
ref={previewRef}
className="prose prose-invert max-w-none"
dangerouslySetInnerHTML={{
__html: content ? renderMarkdownPreview(content) : '<p class="text-gray-500 italic">Start writing to see the preview...</p>'
}}
/>
</div>
</div>
)}
</div>
{/* Settings Sidebar */}
<AnimatePresence>
{showSettings && (
<motion.div
initial={{ x: 320 }}
animate={{ x: 0 }}
exit={{ x: 320 }}
className="w-80 admin-glass-card border-l border-gray-700 flex flex-col"
>
<div className="p-6 border-b border-gray-700">
<h3 className="text-lg font-semibold text-white">Project Settings</h3>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-8">
{/* Status */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Publication</h4>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-white">Published</span>
<button
onClick={() => setPublished(!published)}
className={`w-12 h-6 rounded-full transition-colors relative ${
published ? 'bg-green-600' : 'bg-gray-600'
}`}
>
<div className={`w-4 h-4 bg-white rounded-full transition-transform absolute top-1 ${
published ? 'translate-x-7' : 'translate-x-1'
}`} />
</button>
</div>
<div className="flex items-center justify-between">
<span className="text-white">Featured</span>
<button
onClick={() => setFeatured(!featured)}
className={`w-12 h-6 rounded-full transition-colors relative ${
featured ? 'bg-purple-600' : 'bg-gray-600'
}`}
>
<div className={`w-4 h-4 bg-white rounded-full transition-transform absolute top-1 ${
featured ? 'translate-x-7' : 'translate-x-1'
}`} />
</button>
</div>
</div>
</div>
{/* Category & Difficulty */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Classification</h4>
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">Category</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<div>
<label className="block text-gray-300 text-sm mb-2">Difficulty</label>
<select
value={difficulty}
onChange={(e) => setDifficulty(e.target.value)}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{difficulties.map(diff => (
<option key={diff} value={diff}>{diff}</option>
))}
</select>
</div>
</div>
</div>
{/* Links */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">External Links</h4>
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">
<Github className="w-4 h-4 inline mr-1" />
GitHub Repository
</label>
<input
type="url"
value={github}
onChange={(e) => setGithub(e.target.value)}
placeholder="https://github.com/..."
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-gray-300 text-sm mb-2">
<Globe className="w-4 h-4 inline mr-1" />
Live Demo
</label>
<input
type="url"
value={live}
onChange={(e) => setLive(e.target.value)}
placeholder="https://..."
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
{/* Tags */}
<div>
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider mb-4">Tags</h4>
<div className="space-y-3">
<input
type="text"
placeholder="Add a tag and press Enter"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTag(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
className="w-full px-3 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center space-x-1 px-3 py-1 bg-blue-600 text-white rounded-full text-sm"
>
<span>{tag}</span>
<button
onClick={() => removeTag(tag)}
className="text-blue-200 hover:text-white"
>
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
)}
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
);
};