Files
portfolio/components/EmailManager.tsx
2025-09-10 10:59:14 +02:00

364 lines
13 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
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>
);
};