Initial commit for new panel
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { z } from "zod";
|
||||
|
||||
const ConfirmationModalPropsSchema = z.object({
|
||||
isOpen: z.boolean(),
|
||||
title: z.string().min(1, "Title is required"),
|
||||
message: z.string().min(1, "Message is required"),
|
||||
confirmText: z.string().optional().default("Confirm"),
|
||||
cancelText: z.string().optional().default("Cancel"),
|
||||
onConfirm: z.function(),
|
||||
onCancel: z.function(),
|
||||
variant: z
|
||||
.enum(["default", "danger", "warning"])
|
||||
.optional()
|
||||
.default("default"),
|
||||
size: z.enum(["sm", "md", "lg"]).optional().default("md"),
|
||||
});
|
||||
|
||||
export type ConfirmationModalProps = z.infer<
|
||||
typeof ConfirmationModalPropsSchema
|
||||
>;
|
||||
|
||||
const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
||||
const validatedProps = ConfirmationModalPropsSchema.parse(props);
|
||||
const {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
cancelText,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
variant,
|
||||
size,
|
||||
} = validatedProps;
|
||||
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
return () => setMounted(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && isOpen) {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
document.body.style.overflow = "hidden";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
document.body.style.overflow = "unset";
|
||||
};
|
||||
}, [isOpen, onCancel]);
|
||||
|
||||
if (!mounted || !isOpen) return null;
|
||||
|
||||
const variantStyles = {
|
||||
default: {
|
||||
confirmBtn: "bg-primary hover:bg-primary/90 focus:ring-primary",
|
||||
icon: "🔔",
|
||||
iconBg: "bg-blue-100",
|
||||
},
|
||||
danger: {
|
||||
confirmBtn: "bg-red-light hover:bg-red-light/90 focus:ring-red-light",
|
||||
icon: "⚠️",
|
||||
iconBg: "bg-red-light/20",
|
||||
},
|
||||
warning: {
|
||||
confirmBtn:
|
||||
"bg-yellow-dark hover:bg-yellow-dark/90 focus:ring-yellow-dark",
|
||||
icon: "⚠️",
|
||||
iconBg: "bg-yellow-light/20",
|
||||
},
|
||||
};
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "max-w-sm",
|
||||
md: "max-w-md",
|
||||
lg: "max-w-lg",
|
||||
};
|
||||
|
||||
const currentVariant = variantStyles[variant];
|
||||
const currentSize = sizeStyles[size];
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-50 overflow-y-auto"
|
||||
aria-labelledby="modal-title"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div
|
||||
className="fixed inset-0 bg-dark/70 bg-opacity-75 transition-opacity"
|
||||
onClick={onCancel}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<div
|
||||
className={`relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full ${currentSize} dark:bg-dark-2`}
|
||||
>
|
||||
<div className="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4 dark:bg-dark-2">
|
||||
<div className="sm:flex sm:items-start">
|
||||
<div
|
||||
className={`mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full ${currentVariant.iconBg} sm:mx-0 sm:h-10 sm:w-10`}
|
||||
>
|
||||
<span className="text-xl">{currentVariant.icon}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||
<h3
|
||||
className="text-base font-semibold leading-6 text-dark dark:text-white"
|
||||
id="modal-title"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-dark-5 dark:text-dark-6">
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-dark-2 dark:border-t dark:border-stroke-dark">
|
||||
<button
|
||||
type="button"
|
||||
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${currentVariant.confirmBtn} dark:ring-offset-dark-2`}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{confirmText}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke hover:bg-gray focus:outline-none focus:ring-2 focus:ring-dark-5 focus:ring-offset-2 sm:mt-0 sm:w-auto dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 dark:focus:ring-offset-dark-2"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{cancelText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
export default ConfirmationModal;
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { useClickOutside } from "@/hooks/use-click-outside";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SetStateActionType } from "@/types/set-state-action-type";
|
||||
import {
|
||||
createContext,
|
||||
type PropsWithChildren,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
type DropdownContextType = {
|
||||
isOpen: boolean;
|
||||
handleOpen: () => void;
|
||||
handleClose: () => void;
|
||||
};
|
||||
|
||||
const DropdownContext = createContext<DropdownContextType | null>(null);
|
||||
|
||||
function useDropdownContext() {
|
||||
const context = useContext(DropdownContext);
|
||||
if (!context) {
|
||||
throw new Error("useDropdownContext must be used within a Dropdown");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
type DropdownProps = {
|
||||
children: React.ReactNode;
|
||||
isOpen: boolean;
|
||||
setIsOpen: SetStateActionType<boolean>;
|
||||
};
|
||||
|
||||
export function Dropdown({ children, isOpen, setIsOpen }: DropdownProps) {
|
||||
const triggerRef = useRef<HTMLElement>(null);
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
triggerRef.current = document.activeElement as HTMLElement;
|
||||
|
||||
document.body.style.pointerEvents = "none";
|
||||
} else {
|
||||
document.body.style.removeProperty("pointer-events");
|
||||
|
||||
setTimeout(() => {
|
||||
triggerRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
function handleClose() {
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
function handleOpen() {
|
||||
setIsOpen(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownContext.Provider value={{ isOpen, handleOpen, handleClose }}>
|
||||
<div className="relative" onKeyDown={handleKeyDown}>
|
||||
{children}
|
||||
</div>
|
||||
</DropdownContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
type DropdownContentProps = {
|
||||
align?: "start" | "end" | "center";
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function DropdownContent({
|
||||
children,
|
||||
align = "center",
|
||||
className,
|
||||
}: DropdownContentProps) {
|
||||
const { isOpen, handleClose } = useDropdownContext();
|
||||
|
||||
const contentRef = useClickOutside<HTMLDivElement>(() => {
|
||||
if (isOpen) handleClose();
|
||||
});
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
className={cn(
|
||||
"fade-in-0 zoom-in-95 pointer-events-auto absolute z-99 mt-2 min-w-[8rem] origin-top-right rounded-lg",
|
||||
{
|
||||
"animate-in right-0": align === "end",
|
||||
"left-0": align === "start",
|
||||
"left-1/2 -translate-x-1/2": align === "center",
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DropdownTriggerProps = React.HTMLAttributes<HTMLButtonElement> & {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function DropdownTrigger({ children, className }: DropdownTriggerProps) {
|
||||
const { handleOpen, isOpen } = useDropdownContext();
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={handleOpen}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="menu"
|
||||
data-state={isOpen ? "open" : "closed"}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function DropdownClose({ children }: PropsWithChildren) {
|
||||
const { handleClose } = useDropdownContext();
|
||||
|
||||
return <div onClick={handleClose}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface IProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const ShadowBox = ({ children }: IProps) => {
|
||||
return (
|
||||
<div className="mb-4 mt-2 rounded-3xl border p-4 shadow-xl dark:!border-gray-700 dark:shadow-gray-700">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShadowBox;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"animate-pulse rounded-md bg-neutral-100 dark:bg-dark-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from "react";
|
||||
|
||||
export function Table({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableElement>) {
|
||||
return (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return <thead className={cn("[&_tr]:border-b", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function TableBody({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return (
|
||||
<tbody className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export function TableFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
return (
|
||||
<tfoot
|
||||
className={cn(
|
||||
"border-t bg-neutral-100/50 font-medium dark:bg-neutral-800/50 [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableRow({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLTableRowElement>) {
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-neutral-100/50 data-[state=selected]:bg-neutral-100 dark:border-dark-3 dark:hover:bg-dark-2 dark:data-[state=selected]:bg-neutral-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableHead({
|
||||
className,
|
||||
...props
|
||||
}: React.ThHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<th
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-neutral-500 dark:text-neutral-400 [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableCell({
|
||||
className,
|
||||
...props
|
||||
}: React.TdHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<td
|
||||
className={cn(
|
||||
"p-4 align-middle [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user