✅ Updated API Route Parameters: - Changed parameter type from `{ id: string }` to `Promise<{ id: string }>` in PUT and DELETE methods for better async handling. ✅ Fixed Email Transport Creation: - Updated `nodemailer.createTransporter` to `nodemailer.createTransport` for correct transport configuration. ✅ Refactored AnalyticsDashboard Component: - Changed export from default to named export for better modularity. ✅ Enhanced Email Responder Toast: - Updated toast structure to include additional properties for better user feedback. 🎯 Overall Improvements: - Improved async handling in API routes. - Ensured correct usage of nodemailer. - Enhanced component exports and user notifications.
240 lines
12 KiB
TypeScript
240 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { Toast } from './Toast';
|
|
|
|
interface EmailResponderProps {
|
|
contactEmail: string;
|
|
contactName: string;
|
|
originalMessage: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const EmailResponder: React.FC<EmailResponderProps> = ({
|
|
contactEmail,
|
|
contactName,
|
|
originalMessage,
|
|
onClose
|
|
}) => {
|
|
const [selectedTemplate, setSelectedTemplate] = useState<'welcome' | 'project' | 'quick'>('welcome');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showToast, setShowToast] = useState(false);
|
|
const [toastMessage, setToastMessage] = useState('');
|
|
const [toastType, setToastType] = useState<'success' | 'error'>('success');
|
|
|
|
const templates = {
|
|
welcome: {
|
|
name: 'Willkommen',
|
|
description: 'Freundliche Begrüßung mit Portfolio-Links',
|
|
icon: '👋',
|
|
color: 'from-green-500 to-emerald-600'
|
|
},
|
|
project: {
|
|
name: 'Projekt-Anfrage',
|
|
description: 'Professionelle Antwort für Projekt-Diskussionen',
|
|
icon: '🚀',
|
|
color: 'from-purple-500 to-violet-600'
|
|
},
|
|
quick: {
|
|
name: 'Schnelle Antwort',
|
|
description: 'Kurze, schnelle Bestätigung',
|
|
icon: '⚡',
|
|
color: 'from-amber-500 to-orange-600'
|
|
}
|
|
};
|
|
|
|
const handleSendEmail = async () => {
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const response = await fetch('/api/email/respond', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
to: contactEmail,
|
|
name: contactName,
|
|
template: selectedTemplate,
|
|
originalMessage: originalMessage
|
|
}),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
setToastMessage(`✅ ${data.message}`);
|
|
setToastType('success');
|
|
setShowToast(true);
|
|
setTimeout(() => {
|
|
onClose();
|
|
}, 2000);
|
|
} else {
|
|
setToastMessage(`❌ ${data.error}`);
|
|
setToastType('error');
|
|
setShowToast(true);
|
|
}
|
|
} catch {
|
|
setToastMessage('❌ Fehler beim Senden der E-Mail');
|
|
setToastType('error');
|
|
setShowToast(true);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-2xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
|
|
{/* Header */}
|
|
<div className="bg-gradient-to-r from-blue-600 to-purple-600 text-white p-6 rounded-t-2xl">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-2xl font-bold">📧 E-Mail Antwort senden</h2>
|
|
<p className="text-blue-100 mt-1">Wähle ein schönes Template für deine Antwort</p>
|
|
</div>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-white hover:text-gray-200 transition-colors"
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6">
|
|
|
|
{/* Contact Info */}
|
|
<div className="bg-gray-50 rounded-xl p-4 mb-6">
|
|
<h3 className="font-semibold text-gray-800 mb-2">📬 Kontakt-Informationen</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="text-sm text-gray-600">Name:</span>
|
|
<p className="font-medium text-gray-900">{contactName}</p>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-gray-600">E-Mail:</span>
|
|
<p className="font-medium text-gray-900">{contactEmail}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Original Message Preview */}
|
|
<div className="bg-blue-50 rounded-xl p-4 mb-6">
|
|
<h3 className="font-semibold text-blue-800 mb-2">💬 Ursprüngliche Nachricht</h3>
|
|
<div className="bg-white rounded-lg p-3 border-l-4 border-blue-500">
|
|
<p className="text-gray-700 text-sm whitespace-pre-wrap">{originalMessage}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Template Selection */}
|
|
<div className="mb-6">
|
|
<h3 className="font-semibold text-gray-800 mb-4">🎨 Template auswählen</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
{Object.entries(templates).map(([key, template]) => (
|
|
<div
|
|
key={key}
|
|
className={`relative cursor-pointer rounded-xl border-2 transition-all duration-200 ${
|
|
selectedTemplate === key
|
|
? 'border-blue-500 bg-blue-50 shadow-lg scale-105'
|
|
: 'border-gray-200 hover:border-gray-300 hover:shadow-md'
|
|
}`}
|
|
onClick={() => setSelectedTemplate(key as keyof typeof templates)}
|
|
>
|
|
<div className={`bg-gradient-to-r ${template.color} text-white p-4 rounded-t-xl`}>
|
|
<div className="text-center">
|
|
<div className="text-3xl mb-2">{template.icon}</div>
|
|
<h4 className="font-bold text-lg">{template.name}</h4>
|
|
</div>
|
|
</div>
|
|
<div className="p-4">
|
|
<p className="text-sm text-gray-600 text-center">{template.description}</p>
|
|
</div>
|
|
{selectedTemplate === key && (
|
|
<div className="absolute top-2 right-2">
|
|
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
|
|
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Preview */}
|
|
<div className="mb-6">
|
|
<h3 className="font-semibold text-gray-800 mb-4">👀 Vorschau</h3>
|
|
<div className="bg-gray-100 rounded-xl p-4">
|
|
<div className="bg-white rounded-lg shadow-sm border">
|
|
<div className={`bg-gradient-to-r ${templates[selectedTemplate].color} text-white p-4 rounded-t-lg`}>
|
|
<h4 className="font-bold text-lg">{templates[selectedTemplate].icon} {templates[selectedTemplate].name}</h4>
|
|
<p className="text-sm opacity-90">An: {contactName}</p>
|
|
</div>
|
|
<div className="p-4">
|
|
<p className="text-sm text-gray-600">
|
|
{selectedTemplate === 'welcome' && 'Freundliche Begrüßung mit Portfolio-Links und nächsten Schritten'}
|
|
{selectedTemplate === 'project' && 'Professionelle Projekt-Antwort mit Arbeitsprozess und CTA'}
|
|
{selectedTemplate === 'quick' && 'Schnelle, kurze Bestätigung der Nachricht'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex gap-4">
|
|
<button
|
|
onClick={onClose}
|
|
className="flex-1 px-6 py-3 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 transition-colors font-medium"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={handleSendEmail}
|
|
disabled={isLoading}
|
|
className="flex-1 px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-xl hover:from-blue-700 hover:to-purple-700 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<svg className="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
Sende...
|
|
</>
|
|
) : (
|
|
<>
|
|
📧 E-Mail senden
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Toast */}
|
|
{showToast && (
|
|
<Toast
|
|
toast={{
|
|
id: 'email-toast',
|
|
type: toastType,
|
|
title: toastType === 'success' ? 'E-Mail gesendet!' : 'Fehler!',
|
|
message: toastMessage,
|
|
duration: 5000
|
|
}}
|
|
onRemove={() => setShowToast(false)}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|