Files
portfolio/components/Toast.tsx
Dennis Konkol 9835bb810d 🚀 Complete Production Setup
 Features:
- Analytics Dashboard with real-time metrics
- Redis caching for performance optimization
- Import/Export functionality for projects
- Complete admin system with security
- Production-ready Docker setup

🔧 Technical:
- Removed Ghost CMS dependencies
- Added Redis container with caching
- Implemented API response caching
- Enhanced admin interface with analytics
- Optimized for dk0.dev domain

🛡️ Security:
- Admin authentication with Basic Auth
- Protected analytics endpoints
- Secure environment configuration

📊 Analytics:
- Performance metrics dashboard
- Project statistics visualization
- Real-time data with caching
- Umami integration for GDPR compliance

🎯 Production Ready:
- Multi-container Docker setup
- Health checks for all services
- Automatic restart policies
- Resource limits configured
- Ready for Nginx Proxy Manager
2025-09-05 21:35:54 +00:00

304 lines
8.1 KiB
TypeScript

"use client";
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
CheckCircle,
XCircle,
AlertTriangle,
Info,
X,
Mail,
Database,
Save,
Trash2,
Upload,
Download
} from 'lucide-react';
export type ToastType = 'success' | 'error' | 'warning' | 'info';
export interface Toast {
id: string;
type: ToastType;
title: string;
message: string;
duration?: number;
action?: {
label: string;
onClick: () => void;
};
}
interface ToastProps {
toast: Toast;
onRemove: (id: string) => void;
}
const ToastItem = ({ toast, onRemove }: ToastProps) => {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
if (toast.duration !== 0) {
const timer = setTimeout(() => {
setIsVisible(false);
setTimeout(() => onRemove(toast.id), 300);
}, toast.duration || 5000);
return () => clearTimeout(timer);
}
}, [toast.duration, toast.id, onRemove]);
const getIcon = () => {
switch (toast.type) {
case 'success':
return <CheckCircle className="w-5 h-5 text-green-400" />;
case 'error':
return <XCircle className="w-5 h-5 text-red-400" />;
case 'warning':
return <AlertTriangle className="w-5 h-5 text-yellow-400" />;
case 'info':
return <Info className="w-5 h-5 text-blue-400" />;
default:
return <Info className="w-5 h-5 text-blue-400" />;
}
};
const getColors = () => {
switch (toast.type) {
case 'success':
return 'bg-white border-green-300 text-green-900 shadow-lg';
case 'error':
return 'bg-white border-red-300 text-red-900 shadow-lg';
case 'warning':
return 'bg-white border-yellow-300 text-yellow-900 shadow-lg';
case 'info':
return 'bg-white border-blue-300 text-blue-900 shadow-lg';
default:
return 'bg-white border-gray-300 text-gray-900 shadow-lg';
}
};
return (
<motion.div
initial={{ opacity: 0, y: -50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -50, scale: 0.9 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className={`relative p-4 rounded-xl border ${getColors()} shadow-xl hover:shadow-2xl transition-all duration-300 max-w-sm`}
>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 mt-0.5">
{getIcon()}
</div>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold mb-1">{toast.title}</h4>
<p className="text-sm opacity-90">{toast.message}</p>
{toast.action && (
<button
onClick={toast.action.onClick}
className="mt-2 text-xs font-medium underline hover:no-underline transition-all"
>
{toast.action.label}
</button>
)}
</div>
<button
onClick={() => {
setIsVisible(false);
setTimeout(() => onRemove(toast.id), 300);
}}
className="flex-shrink-0 p-1 rounded-lg hover:bg-gray-100 transition-colors"
>
<X className="w-4 h-4 text-gray-500" />
</button>
</div>
{/* Progress bar */}
{toast.duration !== 0 && (
<motion.div
initial={{ width: '100%' }}
animate={{ width: '0%' }}
transition={{ duration: (toast.duration || 5000) / 1000, ease: "linear" }}
className="absolute bottom-0 left-0 h-1 bg-gradient-to-r from-blue-400 to-green-400 rounded-b-xl"
/>
)}
</motion.div>
);
};
// Toast context and provider
import { createContext, useContext, useCallback } from 'react';
interface ToastContextType {
addToast: (toast: Omit<Toast, 'id'>) => void;
showToast: (toast: Omit<Toast, 'id'>) => void;
showSuccess: (title: string, message?: string) => void;
showError: (title: string, message?: string) => void;
showWarning: (title: string, message?: string) => void;
showInfo: (title: string, message?: string) => void;
showEmailSent: (email: string) => void;
showEmailError: (error: string) => void;
showProjectSaved: (title: string) => void;
showProjectDeleted: (title: string) => void;
showImportSuccess: (count: number) => void;
showImportError: (error: string) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).substr(2, 9);
const newToast = { ...toast, id };
setToasts(prev => [...prev, newToast]);
}, []);
const removeToast = useCallback((id: string) => {
setToasts(prev => prev.filter(toast => toast.id !== id));
}, []);
const showToast = useCallback((toast: Omit<Toast, 'id'>) => {
addToast(toast);
}, [addToast]);
const showSuccess = useCallback((title: string, message?: string) => {
addToast({
type: 'success',
title,
message: message || '',
duration: 4000
});
}, [addToast]);
const showError = useCallback((title: string, message?: string) => {
addToast({
type: 'error',
title,
message: message || '',
duration: 6000
});
}, [addToast]);
const showWarning = useCallback((title: string, message?: string) => {
addToast({
type: 'warning',
title,
message: message || '',
duration: 5000
});
}, [addToast]);
const showInfo = useCallback((title: string, message?: string) => {
addToast({
type: 'info',
title,
message: message || '',
duration: 4000
});
}, [addToast]);
const showEmailSent = useCallback((email: string) => {
addToast({
type: 'success',
title: 'E-Mail gesendet! 📧',
message: `Deine Nachricht an ${email} wurde erfolgreich versendet.`,
duration: 5000,
});
}, [addToast]);
const showEmailError = useCallback((error: string) => {
addToast({
type: 'error',
title: 'E-Mail Fehler! ❌',
message: `Fehler beim Senden: ${error}`,
duration: 8000
});
}, [addToast]);
const showProjectSaved = useCallback((title: string) => {
addToast({
type: 'success',
title: 'Projekt gespeichert! 💾',
message: `"${title}" wurde erfolgreich in der Datenbank gespeichert.`,
duration: 4000,
});
}, [addToast]);
const showProjectDeleted = useCallback((title: string) => {
addToast({
type: 'warning',
title: 'Projekt gelöscht! 🗑️',
message: `"${title}" wurde aus der Datenbank entfernt.`,
duration: 4000,
});
}, [addToast]);
const showImportSuccess = useCallback((count: number) => {
addToast({
type: 'success',
title: 'Import erfolgreich! 📥',
message: `${count} Projekte wurden erfolgreich importiert.`,
duration: 5000,
});
}, [addToast]);
const showImportError = useCallback((error: string) => {
addToast({
type: 'error',
title: 'Import Fehler! ❌',
message: `Fehler beim Importieren: ${error}`,
duration: 8000,
});
}, [addToast]);
const contextValue: ToastContextType = {
addToast,
showToast,
showSuccess,
showError,
showWarning,
showInfo,
showEmailSent,
showEmailError,
showProjectSaved,
showProjectDeleted,
showImportSuccess,
showImportError
};
return (
<ToastContext.Provider value={contextValue}>
{children}
{/* Toast Container */}
<div className="fixed top-4 right-4 z-50 space-y-3 max-w-sm">
<AnimatePresence>
{toasts.map((toast) => (
<ToastItem
key={toast.id}
toast={toast}
onRemove={removeToast}
/>
))}
</AnimatePresence>
</div>
</ToastContext.Provider>
);
};
export default ToastItem;