Files
portfolio/components/EmailManager.tsx
denshooter b44250fe0e 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
2025-09-10 11:06:36 +02:00

615 lines
24 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useState, useEffect } from 'react';
<<<<<<< HEAD
import { EmailResponder } from './EmailResponder';
interface ContactMessage {
id: string;
name: string;
email: string;
subject: string;
message: string;
timestamp: string;
responded: boolean;
}
export const EmailManager: React.FC = () => {
const [messages, setMessages] = useState<ContactMessage[]>([]);
const [selectedMessage, setSelectedMessage] = useState<ContactMessage | null>(null);
const [showResponder, setShowResponder] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState<'all' | 'unread' | 'responded'>('all');
// Mock data for demonstration - in real app, fetch from API
useEffect(() => {
const mockMessages: ContactMessage[] = [
{
id: '1',
name: 'Max Mustermann',
email: 'max@example.com',
subject: 'Projekt-Anfrage',
message: 'Hallo Dennis,\n\nich interessiere mich für eine Zusammenarbeit an einem Web-Projekt. Können wir uns mal unterhalten?\n\nViele Grüße\nMax',
timestamp: new Date().toISOString(),
responded: false
},
{
id: '2',
name: 'Anna Schmidt',
email: 'anna@example.com',
subject: 'Frage zu deinem Portfolio',
message: 'Hi Dennis,\n\nsehr cooles Portfolio! Wie lange hast du an dem Design gearbeitet?\n\nLG Anna',
timestamp: new Date(Date.now() - 86400000).toISOString(),
responded: true
},
{
id: '3',
name: 'Tom Weber',
email: 'tom@example.com',
subject: 'Job-Anfrage',
message: 'Hallo,\n\nwir suchen einen Full-Stack Developer. Bist du interessiert?\n\nTom',
timestamp: new Date(Date.now() - 172800000).toISOString(),
responded: false
}
];
setTimeout(() => {
setMessages(mockMessages);
setIsLoading(false);
}, 1000);
}, []);
const filteredMessages = messages.filter(message => {
switch (filter) {
case 'unread':
return !message.responded;
case 'responded':
return message.responded;
default:
return true;
}
});
const handleRespond = (message: ContactMessage) => {
setSelectedMessage(message);
setShowResponder(true);
};
const handleResponseSent = () => {
if (selectedMessage) {
setMessages(prev => prev.map(msg =>
msg.id === selectedMessage.id
? { ...msg, responded: true }
: msg
));
}
setShowResponder(false);
setSelectedMessage(null);
};
const formatDate = (timestamp: string) => {
return new Date(timestamp).toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
const getMessagePreview = (message: string) => {
return message.length > 100 ? message.substring(0, 100) + '...' : message;
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold text-gray-900">📧 E-Mail Manager</h2>
<p className="text-gray-600 mt-1">Verwalte Kontaktanfragen und sende schöne Antworten</p>
</div>
<div className="flex items-center gap-4">
<div className="text-sm text-gray-600">
{filteredMessages.length} von {messages.length} Nachrichten
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="flex gap-2">
<button
onClick={() => setFilter('all')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
filter === 'all'
? 'bg-blue-100 text-blue-700 border border-blue-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
Alle ({messages.length})
</button>
<button
onClick={() => setFilter('unread')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
filter === 'unread'
? 'bg-red-100 text-red-700 border border-red-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
Ungelesen ({messages.filter(m => !m.responded).length})
</button>
<button
onClick={() => setFilter('responded')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
filter === 'responded'
? 'bg-green-100 text-green-700 border border-green-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
Beantwortet ({messages.filter(m => m.responded).length})
</button>
</div>
</div>
{/* Messages List */}
<div className="space-y-4">
{filteredMessages.length === 0 ? (
<div className="bg-white rounded-xl shadow-sm border p-12 text-center">
<div className="text-6xl mb-4">📭</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">Keine Nachrichten</h3>
<p className="text-gray-600">
{filter === 'unread' && 'Alle Nachrichten wurden beantwortet!'}
{filter === 'responded' && 'Noch keine Nachrichten beantwortet.'}
{filter === 'all' && 'Noch keine Kontaktanfragen eingegangen.'}
</p>
</div>
) : (
filteredMessages.map((message) => (
<div
key={message.id}
className={`bg-white rounded-xl shadow-sm border p-6 transition-all hover:shadow-md ${
!message.responded ? 'border-l-4 border-l-red-500' : 'border-l-4 border-l-green-500'
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<div className={`w-3 h-3 rounded-full ${
message.responded ? 'bg-green-500' : 'bg-red-500'
}`}></div>
<h3 className="font-semibold text-gray-900">{message.name}</h3>
<span className="text-sm text-gray-500">{message.email}</span>
{!message.responded && (
<span className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded-full font-medium">
Neu
</span>
)}
</div>
<h4 className="font-medium text-gray-800 mb-2">{message.subject}</h4>
<p className="text-gray-600 text-sm mb-3 whitespace-pre-wrap">
{getMessagePreview(message.message)}
</p>
<div className="flex items-center gap-4 text-xs text-gray-500">
<span>📅 {formatDate(message.timestamp)}</span>
{message.responded && (
<span className="text-green-600 font-medium"> Beantwortet</span>
)}
</div>
</div>
<div className="flex gap-2 ml-4">
{!message.responded && (
<button
onClick={() => handleRespond(message)}
className="px-4 py-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all font-medium text-sm flex items-center gap-2"
>
📧 Antworten
</button>
)}
<button
onClick={() => {
setSelectedMessage(message);
// Show full message modal
}}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium text-sm"
>
👁 Ansehen
</button>
</div>
</div>
</div>
))
)}
</div>
{/* Email Responder Modal */}
{showResponder && selectedMessage && (
<EmailResponder
contactEmail={selectedMessage.email}
contactName={selectedMessage.name}
originalMessage={selectedMessage.message}
onClose={handleResponseSent}
/>
)}
</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