Feat: Cambios Varios 2
This commit is contained in:
69
frontend/counter-panel/src/context/ToastContext.tsx
Normal file
69
frontend/counter-panel/src/context/ToastContext.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { CheckCircle2, AlertCircle, X, Info } from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { ToastContext, type Toast, type ToastType } from './use-toast';
|
||||
|
||||
export function ToastProvider({ children }: { children: React.ReactNode }) {
|
||||
const [toasts, setToasts] = useState<Toast[]>([]);
|
||||
|
||||
const showToast = useCallback((message: string, type: ToastType) => {
|
||||
const id = Math.random().toString(36).substring(2, 9);
|
||||
setToasts((prev) => [...prev, { id, message, type }]);
|
||||
|
||||
setTimeout(() => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, 5000);
|
||||
}, []);
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast }}>
|
||||
{children}
|
||||
<div className="fixed bottom-8 right-8 z-[1000] flex flex-col gap-3 pointer-events-none">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{toasts.map((toast) => (
|
||||
<motion.div
|
||||
key={toast.id}
|
||||
layout
|
||||
initial={{ opacity: 0, x: 50, scale: 0.8 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, x: 20, scale: 0.5, transition: { duration: 0.2 } }}
|
||||
className={clsx(
|
||||
"pointer-events-auto flex items-center gap-3 px-5 py-4 rounded-2xl shadow-2xl border backdrop-blur-md min-w-[300px] max-w-md",
|
||||
toast.type === 'success' && "bg-emerald-50/90 border-emerald-100 text-emerald-900",
|
||||
toast.type === 'error' && "bg-rose-50/90 border-rose-100 text-rose-900",
|
||||
toast.type === 'info' && "bg-blue-50/90 border-blue-100 text-blue-900"
|
||||
)}
|
||||
>
|
||||
<div className={clsx(
|
||||
"p-2 rounded-xl",
|
||||
toast.type === 'success' && "bg-emerald-500 text-white",
|
||||
toast.type === 'error' && "bg-rose-500 text-white",
|
||||
toast.type === 'info' && "bg-blue-500 text-white"
|
||||
)}>
|
||||
{toast.type === 'success' && <CheckCircle2 size={18} />}
|
||||
{toast.type === 'error' && <AlertCircle size={18} />}
|
||||
{toast.type === 'info' && <Info size={18} />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-black tracking-tight leading-tight">{toast.message}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => removeToast(toast.id)}
|
||||
className="p-1 hover:bg-black/5 rounded-lg transition-colors text-slate-400"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
}
|
||||
21
frontend/counter-panel/src/context/use-toast.ts
Normal file
21
frontend/counter-panel/src/context/use-toast.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'info';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
message: string;
|
||||
type: ToastType;
|
||||
}
|
||||
|
||||
export interface ToastContextType {
|
||||
showToast: (message: string, type: ToastType) => void;
|
||||
}
|
||||
|
||||
export const ToastContext = createContext<ToastContextType | undefined>(undefined);
|
||||
|
||||
export const useToast = () => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) throw new Error('useToast must be used within ToastProvider');
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user