✅ Fixed AdminDashboard sorting types: - Changed aValue/bValue from unknown to string|number|Date ✅ Fixed Toast component: - Removed setIsVisible reference (function doesn't exist) ✅ Fixed Prisma service types: - Added type casting for createProject data - Fixed InteractionType enum (VIEW → BOOKMARK) - Added type casting for analytics/performance data 🎯 Build Status: ✅ SUCCESS - All TypeScript errors resolved - Build completes successfully - 22 routes generated - Ready for production deployment
293 lines
7.9 KiB
TypeScript
293 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import {
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertTriangle,
|
|
Info,
|
|
X,
|
|
} 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) => {
|
|
|
|
useEffect(() => {
|
|
if (toast.duration !== 0) {
|
|
const timer = setTimeout(() => {
|
|
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={() => onRemove(toast.id)}
|
|
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;
|