feat: Website-Rework mit verbessertem Design, Sicherheit und Deployment

- Neue About/Skills-Sektion hinzugefügt
- Verbesserte UI/UX für alle Komponenten
- Enhanced Contact Form mit Validierung
- Verbesserte Security Headers und Middleware
- Sichere Deployment-Skripte (safe-deploy.sh)
- Zero-Downtime Deployment Support
- Verbesserte Docker-Sicherheit
- Umfassende Sicherheits-Dokumentation
- Performance-Optimierungen
- Accessibility-Verbesserungen
This commit is contained in:
2025-11-22 19:24:49 +01:00
parent 498bec6edf
commit 976a6360fd
17 changed files with 1585 additions and 139 deletions

View File

@@ -20,10 +20,48 @@ const Contact = () => {
message: ''
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [touched, setTouched] = useState<Record<string, boolean>>({});
const validateForm = () => {
const newErrors: Record<string, string> = {};
if (!formData.name.trim()) {
newErrors.name = 'Name is required';
} else if (formData.name.trim().length < 2) {
newErrors.name = 'Name must be at least 2 characters';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = 'Please enter a valid email address';
}
if (!formData.subject.trim()) {
newErrors.subject = 'Subject is required';
} else if (formData.subject.trim().length < 3) {
newErrors.subject = 'Subject must be at least 3 characters';
}
if (!formData.message.trim()) {
newErrors.message = 'Message is required';
} else if (formData.message.trim().length < 10) {
newErrors.message = 'Message must be at least 10 characters';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
try {
@@ -43,23 +81,42 @@ const Contact = () => {
if (response.ok) {
showEmailSent(formData.email);
setFormData({ name: '', email: '', subject: '', message: '' });
setTouched({});
setErrors({});
} else {
const errorData = await response.json();
showEmailError(errorData.error || 'Unbekannter Fehler');
showEmailError(errorData.error || 'Failed to send message. Please try again.');
}
} catch (error) {
console.error('Error sending email:', error);
showEmailError('Netzwerkfehler beim Senden der E-Mail');
showEmailError('Network error. Please check your connection and try again.');
} finally {
setIsSubmitting(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData({
...formData,
[e.target.name]: e.target.value
[name]: value
});
// Clear error when user starts typing
if (errors[name]) {
setErrors({
...errors,
[name]: ''
});
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setTouched({
...touched,
[e.target.name]: true
});
validateForm();
};
const contactInfo = [
@@ -159,7 +216,7 @@ const Contact = () => {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
Name
Name <span className="text-red-400">*</span>
</label>
<input
type="text"
@@ -167,15 +224,25 @@ const Contact = () => {
name="name"
value={formData.name}
onChange={handleChange}
onBlur={handleBlur}
required
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all ${
errors.name && touched.name
? 'border-red-500 focus:ring-red-500'
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
}`}
placeholder="Your name"
aria-invalid={errors.name && touched.name ? 'true' : 'false'}
aria-describedby={errors.name && touched.name ? 'name-error' : undefined}
/>
{errors.name && touched.name && (
<p id="name-error" className="mt-1 text-sm text-red-400">{errors.name}</p>
)}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-300 mb-2">
Email
Email <span className="text-red-400">*</span>
</label>
<input
type="email"
@@ -183,16 +250,26 @@ const Contact = () => {
name="email"
value={formData.email}
onChange={handleChange}
onBlur={handleBlur}
required
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all ${
errors.email && touched.email
? 'border-red-500 focus:ring-red-500'
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
}`}
placeholder="your@email.com"
aria-invalid={errors.email && touched.email ? 'true' : 'false'}
aria-describedby={errors.email && touched.email ? 'email-error' : undefined}
/>
{errors.email && touched.email && (
<p id="email-error" className="mt-1 text-sm text-red-400">{errors.email}</p>
)}
</div>
</div>
<div>
<label htmlFor="subject" className="block text-sm font-medium text-gray-300 mb-2">
Subject
Subject <span className="text-red-400">*</span>
</label>
<input
type="text"
@@ -200,39 +277,66 @@ const Contact = () => {
name="subject"
value={formData.subject}
onChange={handleChange}
onBlur={handleBlur}
required
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all ${
errors.subject && touched.subject
? 'border-red-500 focus:ring-red-500'
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
}`}
placeholder="What's this about?"
aria-invalid={errors.subject && touched.subject ? 'true' : 'false'}
aria-describedby={errors.subject && touched.subject ? 'subject-error' : undefined}
/>
{errors.subject && touched.subject && (
<p id="subject-error" className="mt-1 text-sm text-red-400">{errors.subject}</p>
)}
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium text-gray-300 mb-2">
Message
Message <span className="text-red-400">*</span>
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
onBlur={handleBlur}
required
rows={5}
className="w-full px-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all resize-none"
placeholder="Tell me more about your project..."
rows={6}
className={`w-full px-4 py-3 bg-gray-800/50 backdrop-blur-sm border rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 transition-all resize-none ${
errors.message && touched.message
? 'border-red-500 focus:ring-red-500'
: 'border-gray-700 focus:ring-blue-500 focus:border-transparent'
}`}
placeholder="Tell me more about your project or question..."
aria-invalid={errors.message && touched.message ? 'true' : 'false'}
aria-describedby={errors.message && touched.message ? 'message-error' : undefined}
/>
<div className="flex justify-between items-center mt-1">
{errors.message && touched.message ? (
<p id="message-error" className="text-sm text-red-400">{errors.message}</p>
) : (
<span></span>
)}
<span className="text-xs text-gray-500">
{formData.message.length} characters
</span>
</div>
</div>
<motion.button
type="submit"
disabled={isSubmitting}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="w-full btn-primary py-4 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2"
whileHover={!isSubmitting ? { scale: 1.02, y: -2 } : {}}
whileTap={!isSubmitting ? { scale: 0.98 } : {}}
className="w-full btn-primary py-4 text-lg font-semibold disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2 disabled:hover:scale-100 disabled:hover:translate-y-0"
>
{isSubmitting ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
<span>Sending...</span>
<span>Sending Message...</span>
</>
) : (
<>