Add Kelola Data

This commit is contained in:
Randa Firman Putra
2025-07-15 14:46:34 +07:00
parent 833b307602
commit 4585f6a346
28 changed files with 2251 additions and 887 deletions

View File

@@ -0,0 +1,135 @@
"use client";
import React, { createContext, useContext, useState, useCallback } from "react";
import { Toast, ToastTitle, ToastDescription, ToastClose } from "./toast";
import { CheckCircle, AlertCircle, AlertTriangle, Info, X } from "lucide-react";
interface ToastMessage {
id: string;
type: "success" | "error" | "warning" | "info";
title: string;
description?: string;
duration?: number;
}
interface ToastContextType {
showToast: (message: Omit<ToastMessage, "id">) => void;
showSuccess: (title: string, description?: string) => void;
showError: (title: string, description?: string) => void;
showWarning: (title: string, description?: string) => void;
showInfo: (title: string, description?: 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;
};
interface ToastProviderProps {
children: React.ReactNode;
}
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
const showToast = useCallback((message: Omit<ToastMessage, "id">) => {
const id = Math.random().toString(36).substr(2, 9);
const newToast: ToastMessage = {
...message,
id,
duration: message.duration || 5000,
};
setToasts((prev) => [...prev, newToast]);
// Auto remove toast after duration
setTimeout(() => {
removeToast(id);
}, newToast.duration);
}, [removeToast]);
const showSuccess = useCallback((title: string, description?: string) => {
showToast({ type: "success", title, description });
}, [showToast]);
const showError = useCallback((title: string, description?: string) => {
showToast({ type: "error", title, description });
}, [showToast]);
const showWarning = useCallback((title: string, description?: string) => {
showToast({ type: "warning", title, description });
}, [showToast]);
const showInfo = useCallback((title: string, description?: string) => {
showToast({ type: "info", title, description });
}, [showToast]);
const getToastVariant = (type: ToastMessage["type"]) => {
switch (type) {
case "success":
return "success";
case "error":
return "destructive";
case "warning":
return "warning";
case "info":
return "info";
default:
return "default";
}
};
const getToastIcon = (type: ToastMessage["type"]) => {
switch (type) {
case "success":
return <CheckCircle className="h-5 w-5 text-green-600" />;
case "error":
return <AlertCircle className="h-5 w-5 text-red-600" />;
case "warning":
return <AlertTriangle className="h-5 w-5 text-yellow-600" />;
case "info":
return <Info className="h-5 w-5 text-blue-600" />;
default:
return null;
}
};
return (
<ToastContext.Provider
value={{ showToast, showSuccess, showError, showWarning, showInfo }}
>
{children}
{/* Toast Container */}
<div className="fixed top-4 right-4 z-50 space-y-2">
{toasts.map((toast) => (
<Toast
key={toast.id}
variant={getToastVariant(toast.type)}
className="min-w-[300px]"
>
<div className="flex items-start space-x-3">
{getToastIcon(toast.type)}
<div className="flex-1">
<ToastTitle>{toast.title}</ToastTitle>
{toast.description && (
<ToastDescription>{toast.description}</ToastDescription>
)}
</div>
</div>
<ToastClose onClick={() => removeToast(toast.id)} />
</Toast>
))}
</div>
</ToastContext.Provider>
);
};