update 11 files

This commit is contained in:
faisolavolut 2025-01-31 13:43:28 +07:00
parent 98598fa984
commit 2c1acf2007
18 changed files with 2303 additions and 162 deletions

View File

@ -3,7 +3,6 @@ import { FieldCheckbox } from "./field/TypeCheckbox";
import { TypeDropdown } from "./field/TypeDropdown"; import { TypeDropdown } from "./field/TypeDropdown";
import { TypeInput } from "./field/TypeInput"; import { TypeInput } from "./field/TypeInput";
import { TypeUpload } from "./field/TypeUpload"; import { TypeUpload } from "./field/TypeUpload";
import { FieldUploadMulti } from "./field/TypeUploadMulti";
import { TypeRichText } from "./field/TypeRichText"; import { TypeRichText } from "./field/TypeRichText";
import { TypeTag } from "./field/TypeTag"; import { TypeTag } from "./field/TypeTag";
import get from "lodash.get"; import get from "lodash.get";
@ -23,6 +22,7 @@ export const Field: React.FC<any> = ({
hidden_label, hidden_label,
onChange, onChange,
className, className,
classField,
style, style,
prefix, prefix,
suffix, suffix,
@ -98,11 +98,16 @@ export const Field: React.FC<any> = ({
{!hidden_label ? ( {!hidden_label ? (
<label <label
className={cx( className={cx(
"block mb-2 text-md font-medium text-gray-900 text-sm", "block mb-2 text-md font-medium text-gray-900 text-sm flex flex-row",
style === "inline" ? "w-[100px]" : "" style === "inline" ? "w-[100px]" : ""
)} )}
> >
{label} {label}
{required ? (
<span className="flex flex-row px-0.5 text-red-500">*</span>
) : (
<></>
)}
</label> </label>
) : ( ) : (
<></> <></>
@ -137,7 +142,8 @@ export const Field: React.FC<any> = ({
["upload"].includes(type) && ["upload"].includes(type) &&
css` css`
padding: 0px !important; padding: 0px !important;
` `,
classField
)} )}
> >
{before && ( {before && (

View File

@ -1,4 +1,5 @@
"use client"; "use client";
import { ScrollArea } from "../ui/scroll-area";
import { Form } from "./Form"; import { Form } from "./Form";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -21,14 +22,15 @@ export const FormBetter: React.FC<any> = ({
return ( return (
<div className="flex flex-col flex-grow gap-y-3"> <div className="flex flex-col flex-grow gap-y-3">
{typeof fm === "object" && typeof onTitle === "function" ? ( {typeof fm === "object" && typeof onTitle === "function" ? (
<div className="flex flex-row p-3 items-center bg-white border border-gray-300 rounded-lg"> <div className="flex flex-row p-3 items-center rounded-lg">
{onTitle(fm)} {onTitle(fm)}
</div> </div>
) : ( ) : (
<></> <></>
)} )}
<div className="w-full flex-grow flex flex-row rounded-lg overflow-hidden"> <div className="w-full flex-grow flex flex-row rounded-lg">
<div className="w-full flex flex-row flex-grow bg-white rounded-lg border border-gray-300 relative overflow-y-scroll"> <div className="w-full flex flex-row flex-grow bg-white rounded-lg border border-gray-300 relative">
<ScrollArea className="flex-grow">
<Form <Form
{...{ {...{
children, children,
@ -39,7 +41,7 @@ export const FormBetter: React.FC<any> = ({
onFooter, onFooter,
showResize, showResize,
mode, mode,
className: cx(className, "absolute top-0 left-0 w-full"), className: cx(className, "top-0 left-0 w-full"),
onInit: (form: any) => { onInit: (form: any) => {
setFM(form); setFM(form);
@ -65,6 +67,7 @@ export const FormBetter: React.FC<any> = ({
}, },
}} }}
/> />
</ScrollArea>
</div> </div>
</div> </div>
</div> </div>

View File

@ -117,7 +117,13 @@ export const ThumbPreview = ({
); );
}; };
export const FilePreview = ({ url }: { url: any }) => { export const FilePreview = ({
url,
disabled,
}: {
url: any;
disabled?: boolean;
}) => {
let ural = url; let ural = url;
if (url instanceof File) { if (url instanceof File) {
ural = `${URL.createObjectURL(url)}.${url.name.split(".").pop()}`; ural = `${URL.createObjectURL(url)}.${url.name.split(".").pop()}`;
@ -211,7 +217,7 @@ export const FilePreview = ({ url }: { url: any }) => {
{file.extension && ( {file.extension && (
<div <div
className={cx( className={cx(
"flex rounded items-center px-1 bg-white cursor-pointer flex-grow hover:bg-gray-100 gap-x-1", "flex rounded items-center px-1 cursor-pointer flex-grow hover:bg-gray-100 gap-x-1 justify-between",
"pr-2", "pr-2",
css` css`
&:hover { &:hover {
@ -222,7 +228,8 @@ export const FilePreview = ({ url }: { url: any }) => {
// border-bottom: 1px solid #1c4ed8; // border-bottom: 1px solid #1c4ed8;
// outline: 1px solid #1c4ed8; // outline: 1px solid #1c4ed8;
} }
` `,
disabled ? "bg-transparent" : "bg-white"
)} )}
onClick={() => { onClick={() => {
let _url: any = let _url: any =
@ -231,8 +238,10 @@ export const FilePreview = ({ url }: { url: any }) => {
window.open(_url, "_blank"); window.open(_url, "_blank");
}} }}
> >
<div className="flex flex-row gap-x-1 items-center">
<div className="h-[30px] flex flex-row items-center">{content}</div> <div className="h-[30px] flex flex-row items-center">{content}</div>
<div className="text-xs filename">{file?.name}</div> <div className="text-xs filename">{file?.name}</div>
</div>
<div className="ml-2"> <div className="ml-2">
<ExternalLink size="12px" /> <ExternalLink size="12px" />

View File

@ -5,7 +5,7 @@ import { Textarea } from "../../ui/text-area";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import tinycolor from "tinycolor2"; import tinycolor from "tinycolor2";
import { FieldColorPicker } from "../../ui/FieldColorPopover"; import { FieldColorPicker } from "../../ui/FieldColorPopover";
import { FaRegStar, FaStar } from "react-icons/fa6"; import { FaRegEye, FaRegEyeSlash, FaRegStar, FaStar } from "react-icons/fa6";
import { Rating } from "../../ui/ratings"; import { Rating } from "../../ui/ratings";
import { getNumber } from "@/lib/utils/getNumber"; import { getNumber } from "@/lib/utils/getNumber";
import MaskedInput from "../../ui/MaskedInput"; import MaskedInput from "../../ui/MaskedInput";
@ -34,6 +34,7 @@ export const TypeInput: React.FC<any> = ({
const input = useLocal({ const input = useLocal({
value: 0 as any, value: 0 as any,
ref: null as any, ref: null as any,
show_pass: false as boolean,
open: false, open: false,
}); });
const meta = useLocal({ const meta = useLocal({
@ -319,6 +320,10 @@ export const TypeInput: React.FC<any> = ({
); );
break; break;
} }
let type_field = type;
if (input.show_pass) {
type_field = "text";
}
return ( return (
<> <>
<Input <Input
@ -341,7 +346,7 @@ export const TypeInput: React.FC<any> = ({
required={required} required={required}
placeholder={placeholder || ""} placeholder={placeholder || ""}
value={value} value={value}
type={!type ? "text" : type} type={!type ? "text" : type_field}
onChange={(ev) => { onChange={(ev) => {
fm.data[name] = ev.currentTarget.value; fm.data[name] = ev.currentTarget.value;
fm.render(); fm.render();
@ -350,6 +355,24 @@ export const TypeInput: React.FC<any> = ({
} }
}} }}
/> />
{type === "password" && (
<div
className=" right-0 h-full flex items-center cursor-pointer px-2"
onClick={() => {
input.show_pass = !input.show_pass;
input.render();
}}
>
<div className="">
{input.show_pass ? (
<FaRegEyeSlash className="h-4" />
) : (
<FaRegEye className="h-4" />
)}
</div>
</div>
)}
</> </>
); );
}; };

View File

@ -18,7 +18,6 @@ import TableCell from "@tiptap/extension-table-cell";
import TableHeader from "@tiptap/extension-table-header"; import TableHeader from "@tiptap/extension-table-header";
import TableRow from "@tiptap/extension-table-row"; import TableRow from "@tiptap/extension-table-row";
import { ButtonRichText } from "../../ui/button-rich-text"; import { ButtonRichText } from "../../ui/button-rich-text";
import { InitEditor } from "./AfterEditor";
export const TypeRichText: React.FC<any> = ({ export const TypeRichText: React.FC<any> = ({
name, name,
@ -822,7 +821,7 @@ export const TypeRichText: React.FC<any> = ({
onChange(fm.data[name]); onChange(fm.data[name]);
} }
}} }}
content={input.value} content={fm.data?.[name]}
editable={!disabled} editable={!disabled}
></EditorProvider> ></EditorProvider>
</div> </div>

View File

@ -199,7 +199,10 @@ export const FieldUploadSingle: FC<{
</div> </div>
) : input.fase === "preview" ? ( ) : input.fase === "preview" ? (
<div className="flex flex-row gap-x-1 justify-between flex-1 p-1"> <div className="flex flex-row gap-x-1 justify-between flex-1 p-1">
<FilePreview url={input.isLocal ? input.preview : value || ""} /> <FilePreview
url={input.isLocal ? input.preview : value || ""}
disabled={disabled}
/>
{!disabled ? ( {!disabled ? (
<> <>
<div <div

View File

@ -11,6 +11,7 @@ import { detectCase } from "@/lib/utils/detectCase";
import { useLocal } from "@/lib/utils/use-local"; import { useLocal } from "@/lib/utils/use-local";
import { get_user } from "@/lib/utils/get_user"; import { get_user } from "@/lib/utils/get_user";
import { siteurl } from "@/lib/utils/siteurl"; import { siteurl } from "@/lib/utils/siteurl";
import { ScrollArea } from "../ui/scroll-area";
interface TreeMenuItem { interface TreeMenuItem {
title: string; title: string;
href?: string; href?: string;
@ -320,7 +321,7 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
<Sidebar <Sidebar
aria-label="Sidebar with multi-level dropdown example" aria-label="Sidebar with multi-level dropdown example"
className={classNames( className={classNames(
"relative pt-0 sidebar rounded-none", "relative pt-0 rounded-none",
mini ? "w-20" : "", mini ? "w-20" : "",
css` css`
> div { > div {
@ -332,8 +333,9 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
` `
)} )}
> >
<ScrollArea className="w-full h-full">
<div className="w-full h-full relative "> <div className="w-full h-full relative ">
<div className="flex h-full flex-col justify-between w-full absolute top-0 left-0"> <div className="flex h-full flex-col justify-between w-full">
<Sidebar.Items> <Sidebar.Items>
<Sidebar.ItemGroup <Sidebar.ItemGroup
className={cx( className={cx(
@ -346,6 +348,7 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
</Sidebar.Items> </Sidebar.Items>
</div> </div>
</div> </div>
</ScrollArea>
</Sidebar> </Sidebar>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">

View File

@ -0,0 +1,775 @@
"use client";
import React from "react";
import { Avatar, Sidebar } from "flowbite-react";
import { useEffect, useState } from "react";
import classNames from "classnames";
import { css } from "@emotion/css";
import { FaChevronDown, FaChevronUp } from "react-icons/fa";
import { SidebarLinkBetter } from "../ui/link-better";
import { detectCase } from "@/lib/utils/detectCase";
import { useLocal } from "@/lib/utils/use-local";
import { get_user } from "@/lib/utils/get_user";
import { siteurl } from "@/lib/utils/siteurl";
import { ScrollArea } from "../ui/scroll-area";
import { LuChevronsLeftRight } from "react-icons/lu";
import { Popover } from "../Popover/Popover";
import { HiEye } from "react-icons/hi";
import { MdOutlineMoreVert } from "react-icons/md";
import api from "@/lib/utils/axios";
import { PiUserSwitch } from "react-icons/pi";
import { IoIosLogOut } from "react-icons/io";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
interface TreeMenuItem {
title: string;
href?: string;
children?: TreeMenuItem[];
icon?: any;
}
interface TreeMenuProps {
data: TreeMenuItem[];
minimaze: () => void;
mini: boolean;
}
const SidebarBetterTree: React.FC<TreeMenuProps> = ({
data,
minimaze,
mini,
}) => {
const [currentPage, setCurrentPage] = useState("");
const [notification, setNotification] = useState(false as boolean);
const [profile, setProfile] = useState(false as boolean);
const local = useLocal({
data: data,
ready: false as boolean,
});
useEffect(() => {
if (typeof location === "object") {
const newPage = window.location.pathname;
setCurrentPage(newPage);
}
const run = async () => {
local.ready = false;
local.render();
setTimeout(() => {
local.ready = true;
local.render();
}, 1000);
};
if (typeof window === "object") {
run();
}
}, []);
const isChildActive = (items: TreeMenuItem[]): boolean => {
return items.some((item) => {
if (item.href && currentPage.startsWith(item.href)) return true;
if (item.children) return isChildActive(item.children); // Rekursif
return false;
});
};
const renderTree = (items: TreeMenuItem[], depth: number = 0) => {
return items.map((item, index) => {
const hasChildren = item.children && item.children.length > 0;
let isActive = item.href && detectCase(currentPage, item.href);
let isParentActive = hasChildren && isChildActive(item.children!);
const [isOpen, setIsOpen] = useState(isParentActive);
useEffect(() => {
if (isParentActive) {
setIsOpen(true);
}
}, [isParentActive]);
const itemStyle = {
paddingLeft:
mini && !depth
? "10px"
: !hasChildren && !depth
? "13px"
: !mini
? `${depth * 16}px`
: "0px",
};
return (
<React.Fragment key={item.href || item.title || index}>
{hasChildren ? (
<li className="relative ">
<div className="w-full flex flex-row items-center justify-center">
<div
className={cx(
mini ? "flex flex-row" : "flex flex-row flex-grow"
)}
>
{mini && !depth ? (
<>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div
className={classNames(
"transition-all font-bold text-sidebar-label relative flex-row flex items-center cursor-pointer items-center text-base flex flex-row ",
isActive
? " text-base"
: " hover:bg-card-layer hover:shadow-md hover:text-primary",
mini
? "transition-all duration-200 justify-center ml-0 rounded-lg"
: "py-2.5 px-4 rounded-md flex-grow mx-2 pr-0",
!depth && !hasChildren ? "px-2" : "",
css`
& > span {
white-space: wrap !important;
}
`,
mini
? isActive
? !depth
? "bg-linear-sidebar-active text-white font-normal p-1 shadow-md"
: " bg-layer text-primary font-bold p-1 "
: "text-primary bg-transparent p-1 hover:bg-card-layer hover:shadow-md w-[50px]"
: isActive
? !depth
? " bg-linear-sidebar-active font-bold text-white shadow-md "
: " bg-layer text-primary font-bold "
: "text-white"
)}
onClick={() => {
if (mini) {
minimaze();
}
setIsOpen(!isOpen);
}}
style={mini ? {} : itemStyle}
>
<div
className={cx(
"flex flex-row items-center flex-grow",
mini
? " justify-center rounded-md "
: " px-3",
mini
? isParentActive
? "bg-linear-sidebar-active text-white font-bold "
: "text-primary"
: isActive
? "font-bold "
: ""
)}
>
{!depth ? (
<div
className={classNames(
" w-8 h-8 text-center flex flex-row items-center justify-center",
!mini ? "mr-1 p-2 " : " text-lg ",
mini
? css`
background: transparent !important;
`
: ``
)}
>
{item.icon}
</div>
) : (
<></>
)}
{!mini ? (
<>
<div className="pl-2 flex-grow text-xs line-clamp-2">
{item.title}
</div>
<div className="text-md px-1">
{isOpen ? (
<FaChevronUp />
) : (
<FaChevronDown />
)}
</div>
</>
) : (
<></>
)}
</div>
</div>
</TooltipTrigger>
<TooltipContent
className="bg-linear-sidebar-active text-white border border-primary shadow-md transition-all "
side="right"
>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
) : (
<>
<div
className={classNames(
"transition-all font-bold text-sidebar-label relative flex-row flex items-center cursor-pointer items-center text-base flex flex-row ",
isActive
? " text-base"
: " hover:bg-card-layer hover:shadow-md hover:text-primary",
mini
? "transition-all duration-200 justify-center ml-0 rounded-lg"
: "py-2.5 px-4 rounded-md flex-grow mx-2 pr-0",
!depth && !hasChildren ? "px-2" : "",
css`
& > span {
white-space: wrap !important;
}
`,
mini
? isActive
? !depth
? "bg-linear-sidebar-active text-white font-normal p-1 shadow-md"
: " bg-layer text-primary font-bold p-1 "
: "text-primary bg-transparent p-1 hover:bg-card-layer hover:shadow-md w-[50px]"
: isActive
? !depth
? " bg-linear-sidebar-active font-bold text-white shadow-md "
: " bg-layer text-primary font-bold "
: "text-sidebar-label"
)}
onClick={() => {
if (mini) {
minimaze();
}
setIsOpen(!isOpen);
}}
style={mini ? {} : itemStyle}
>
<div
className={cx(
"flex flex-row items-center flex-grow",
mini ? " justify-center rounded-md " : " px-3",
mini
? isParentActive
? "bg-linear-sidebar-active text-white font-bold "
: "text-primary"
: isActive
? "font-bold "
: ""
)}
>
{!depth ? (
<div
className={classNames(
" w-8 h-8 text-center flex flex-row items-center justify-center",
!mini ? "mr-1 p-2 " : " text-lg ",
mini
? css`
background: transparent !important;
`
: ``
)}
>
{item.icon}
</div>
) : (
<></>
)}
{!mini ? (
<>
<div className="pl-2 flex-grow text-xs line-clamp-2">
{item.title}
</div>
<div className="text-md px-1">
{isOpen ? <FaChevronUp /> : <FaChevronDown />}
</div>
</>
) : (
<></>
)}
</div>
</div>
</>
)}
</div>
</div>
<Sidebar.ItemGroup
className={classNames(
"border-none mt-0",
isOpen ? "" : "hidden",
mini ? "hidden" : ""
)}
>
{renderTree(item.children!, depth + 1)}
</Sidebar.ItemGroup>
</li>
) : (
<li className="relative">
<div className="w-full flex flex-row items-center justify-center">
<div
className={cx(
mini ? "flex flex-row" : "flex flex-row w-full"
)}
>
{mini ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<SidebarLinkBetter
href={item.href}
onClick={() => {
if (item?.href) setCurrentPage(item.href);
}}
className={classNames(
"transition-all font-bold relative flex-row flex items-center cursor-pointer items-center text-base flex flex-row ",
isActive
? " text-base"
: "hover:bg-card-layer hover:shadow-md hover:text-primary",
mini
? "transition-all duration-200 justify-center ml-0 rounded-lg"
: "py-2.5 px-4 rounded-md flex-grow mx-2",
!depth && !hasChildren ? "px-2" : "",
css`
& > span {
white-space: wrap !important;
}
`,
mini
? isActive
? !depth
? "bg-linear-sidebar-active text-white font-normal p-1 shadow-md"
: " bg-layer text-primary font-bold p-1 "
: "text-primary bg-transparent p-1 hover:bg-card-layer hover:shadow-md"
: isActive
? !depth
? " bg-linear-sidebar-active font-bold text-white shadow-md"
: " bg-linear-sidebar-active text-white font-bold "
: "text-sidebar-label"
)}
style={mini ? {} : itemStyle} // Terapkan gaya berdasarkan depth
>
<div className="flex flex-row items-center">
{!depth ? (
<div
className={classNames(
" text-dark-700 w-8 h-8 rounded-lg text-center flex flex-row items-center justify-center",
!mini ? "mr-1 p-2" : " text-lg"
)}
>
{item.icon}
</div>
) : (
<></>
)}
{!mini ? (
<>
<div className="pl-2 text-xs line-clamp-1">
{item.title}
</div>
</>
) : (
<></>
)}
</div>
</SidebarLinkBetter>
</div>
</TooltipTrigger>
<TooltipContent
className="bg-linear-sidebar-active text-white border border-primary shadow-md transition-all "
side="right"
>
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<SidebarLinkBetter
href={item.href}
onClick={() => {
if (item?.href) setCurrentPage(item.href);
}}
className={classNames(
"transition-all font-bold relative flex-row flex items-center cursor-pointer items-center text-base flex flex-row ",
isActive
? " text-base"
: "hover:bg-card-layer hover:shadow-md hover:text-primary",
mini
? "transition-all duration-200 justify-center ml-0 rounded-lg"
: "py-2.5 px-4 rounded-md flex-grow mx-2",
!depth && !hasChildren ? "px-2" : "",
css`
& > span {
white-space: wrap !important;
}
`,
mini
? isActive
? !depth
? "bg-linear-sidebar-active text-white font-normal p-1 shadow-md"
: " bg-layer text-primary font-bold p-1 "
: "text-primary bg-transparent p-1 hover:bg-card-layer hover:shadow-md"
: isActive
? !depth
? " bg-linear-sidebar-active font-bold text-white shadow-md"
: " bg-linear-sidebar-active text-white font-bold "
: "text-sidebar-label"
)}
style={mini ? {} : itemStyle} // Terapkan gaya berdasarkan depth
>
<div className="flex flex-row items-center">
{!depth ? (
<div
className={classNames(
" text-dark-700 w-8 h-8 rounded-lg text-center flex flex-row items-center justify-center",
!mini ? "mr-1 p-2" : " text-lg"
)}
>
{item.icon}
</div>
) : (
<></>
)}
{!mini ? (
<>
<div className="pl-2 text-xs line-clamp-1">
{item.title}
</div>
</>
) : (
<></>
)}
</div>
</SidebarLinkBetter>
)}
</div>
</div>
</li>
)}
</React.Fragment>
);
});
};
return (
<div className="flex flex-col flex-grow bg-white relative rounded-b-2xl">
<div
className={cx(
"bg-linear-sidebar-active text-white p-1 absolute top-0 right-[-13px] cursor-pointer rounded-lg shadow-md",
css`
z-index: 1;
`
)}
onClick={() => {
minimaze();
localStorage.setItem("mini", !mini ? "true" : "false");
}}
>
<LuChevronsLeftRight />
</div>
<div className={classNames("flex flex-grow lg:!block ", {})}>
<Sidebar
aria-label="Sidebar with multi-level dropdown example"
className={classNames(
"relative pt-0 rounded-none",
mini ? "w-20" : "",
css`
> div {
border-radius: 0px;
background: transparent;
padding-top: 0;
padding-right: 0;
padding-left: 0;
padding-bottom: 0;
}
`
)}
>
<ScrollArea className="w-full h-full">
<div className="w-full h-full relative ">
<div className="flex h-full flex-col w-full">
<Sidebar.Items>
<Sidebar.ItemGroup
className={cx(
"border-none mt-0 pt-4",
mini ? "flex flex-col gap-y-2" : ""
)}
>
{renderTree(data)}
</Sidebar.ItemGroup>
</Sidebar.Items>
</div>
</div>
</ScrollArea>
</Sidebar>
</div>
<div className="flex flex-col gap-y-4">
<div
className={cx(
"flex flex-col flex-grow gap-y-2",
mini ? "justify-center" : "px-4"
)}
>
<div className="flex flex-row justify-center flex-grow">
<Popover
classNameTrigger={cx("flex flex-row justify-center flex-grow")}
popoverClassName={cx(
"max-w-[24rem] bg-white shadow-lg rounded-lg"
)}
placement={"right-start"}
arrow={false}
className="rounded-md w-full "
onOpenChange={(open: any) => {
setNotification(open);
}}
open={notification}
content={
<div className="max-w-[24rem]">
<div className="block rounded-t-xl bg-gray-50 py-2 px-4 text-center text-base font-medium text-gray-700">
Notifications
</div>
<div className="flex flex-col">
<a
href="#"
className="flex border-y py-3 px-4 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
New message from&nbsp;
<span className="font-semibold text-gray-900 ">
Bonnie Green
</span>
: "Hey, what's up? All set for the presentation?"
</div>
<div className="text-md font-medium text-primary-700 ">
a few moments ago
</div>
</div>
</a>
<a
href="#"
className="flex border-y py-3 px-4 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
New message from&nbsp;
<span className="font-semibold text-gray-900 ">
Bonnie Green
</span>
: "Hey, what's up? All set for the presentation?"
</div>
<div className="text-md font-medium text-primary-700 ">
a few moments ago
</div>
</div>
</a>
<a
href="#"
className="flex border-y py-3 px-4 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
New message from&nbsp;
<span className="font-semibold text-gray-900 ">
Bonnie Green
</span>
: "Hey, what's up? All set for the presentation?"
</div>
<div className="text-md font-medium text-primary-700 ">
a few moments ago
</div>
</div>
</a>
<a
href="#"
className="flex border-y py-3 px-4 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
New message from&nbsp;
<span className="font-semibold text-gray-900 ">
Bonnie Green
</span>
: "Hey, what's up? All set for the presentation?"
</div>
<div className="text-md font-medium text-primary-700 ">
a few moments ago
</div>
</div>
</a>
</div>
<a
href="#"
className="block rounded-b-xl bg-gray-50 py-2 text-center text-base font-normal text-gray-900 hover:bg-gray-100 dark:bg-gray-700 dark:hover:underline"
>
<div className="inline-flex items-center gap-x-2">
<HiEye className="h-6 w-6" />
<span>View all</span>
</div>
</a>
</div>
}
>
<div
className={cx(
"flex flex-row items-center gap-x-2 items-center text-sm rounded-md font-bold",
mini
? "justify-center text-primary w-50 p-1 px-2"
: "text-sidebar-label w-full p-2",
false ? "" : "hover:bg-card-layer hover:shadow-md"
)}
>
<div
className={classNames(
" w-8 h-8 text-center flex flex-row items-center justify-center ",
!mini ? "mr-1 p-2 " : " text-lg ",
mini
? css`
background: transparent !important;
`
: ``
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width={20}
height={20}
viewBox="0 0 24 24"
>
<g fill="none" stroke="currentColor" strokeWidth={2}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 19v-9a6 6 0 0 1 6-6v0a6 6 0 0 1 6 6v9M6 19h12M6 19H4m14 0h2m-9 3h2"
></path>
<circle cx={12} cy={3} r={1}></circle>
</g>
</svg>
</div>
{!mini && "Notifications"}
</div>
</Popover>
</div>
<div
className={cx(
"flex flex-row w-full p-2 items-center gap-x-2 items-center text-white text-sm hidden",
mini ? "justify-center" : ""
)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width={20}
height={20}
viewBox="0 0 24 24"
>
<g fill="currentColor">
<path d="M6.5 3.75c-.526 0-1.25.63-1.25 1.821V18.43c0 1.192.724 1.821 1.25 1.821h6.996a.75.75 0 1 1 0 1.5H6.5c-1.683 0-2.75-1.673-2.75-3.321V5.57c0-1.648 1.067-3.321 2.75-3.321h7a.75.75 0 0 1 0 1.5z"></path>
<path d="M16.53 7.97a.75.75 0 0 0-1.06 0v3.276H9.5a.75.75 0 0 0 0 1.5h5.97v3.284a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0 .22-.532v-.002a.75.75 0 0 0-.269-.575z"></path>
</g>
</svg>
{!mini && "Sign Out"}
</div>
</div>
<div className="flex flex-row w-full p-4 pt-0">
<div
className={cx(
"flex flex-row justify-center flex-grow rounded-lg",
mini ? "py-4" : "p-4 bg-primary "
)}
>
<Avatar alt="" img={siteurl("/dog.jpg")} rounded size="sm" />
{!mini && (
<div className="flex flex-row items-center flex-grow font-bold text-white">
<div className=" px-2 h-full flex flex-col text-xs flex-grow">
<div>
{get_user("employee.name")
? get_user("employee.name")
: "-"}
</div>
<div className="font-normal">
{get_user("email") ? get_user("email") : "-"}
</div>
</div>
<Popover
classNameTrigger={cx("flex flex-row justify-center")}
popoverClassName={cx(
"max-w-[24rem] bg-white shadow-lg rounded-lg"
)}
placement={"right-start"}
arrow={false}
className="rounded-md w-full "
onOpenChange={(open: any) => {
setProfile(open);
}}
open={profile}
content={
<div className="max-w-[24rem] rounded-xl overflow-hidden">
<div className="block bg-gray-50 py-2 px-4 text-sm font-medium text-gray-700">
Profile
</div>
<div className="flex flex-col">
<div
onClick={async () => {
await api.delete(
process.env.NEXT_PUBLIC_BASE_URL +
"/api/destroy-cookies"
);
localStorage.removeItem("user");
if (typeof window === "object")
navigate(
`${process.env.NEXT_PUBLIC_API_PORTAL}/logout`
);
}}
className="cursor-pointer px-4 py-2 flex border-y hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="w-full">
<div className="text-sm items-center font-normal text-primary-700 flex flex-row gap-x-2">
<PiUserSwitch />
Switch Role
</div>
</div>
</div>
<div
onClick={async () => {
await api.delete(
process.env.NEXT_PUBLIC_BASE_URL +
"/api/destroy-cookies"
);
localStorage.removeItem("user");
if (typeof window === "object")
navigate(
`${process.env.NEXT_PUBLIC_API_PORTAL}/logout`
);
}}
className="cursor-pointer px-4 py-2 flex hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="w-full">
<div className="text-sm items-center font-normal text-primary-700 flex flex-row gap-x-2">
<IoIosLogOut />
Sign Out
</div>
</div>
</div>
</div>
</div>
}
>
<div className="text-white cursor-pointer h-full flex flex-row items-center">
<MdOutlineMoreVert />
</div>
</Popover>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default SidebarBetterTree;

View File

@ -1,33 +1,13 @@
"use client"; "use client";
import { import React, { useEffect, useState } from "react";
ColumnDef, import { Table } from "flowbite-react";
ColumnResizeDirection, import { HiChevronLeft, HiChevronRight } from "react-icons/hi";
ColumnResizeMode,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { useCallback, useEffect, useState } from "react";
import { Button, Label, Table } from "flowbite-react";
import { HiChevronLeft, HiChevronRight, HiPlus } from "react-icons/hi";
import { useLocal } from "@/lib/utils/use-local"; import { useLocal } from "@/lib/utils/use-local";
import { debouncedHandler } from "@/lib/utils/debounceHandler";
import { FaChevronUp } from "react-icons/fa6";
import Link from "next/link";
import { init_column } from "./lib/column"; import { init_column } from "./lib/column";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, Sticker } from "lucide-react"; import { Loader2, Sticker } from "lucide-react";
import { InputSearch } from "../ui/input-search";
import { FaChevronDown } from "react-icons/fa";
import get from "lodash.get";
import { Checkbox } from "../ui/checkbox";
import { getNumber } from "@/lib/utils/getNumber"; import { getNumber } from "@/lib/utils/getNumber";
import { formatMoney } from "../form/field/TypeInput"; import { formatMoney } from "../form/field/TypeInput";
import { cloneFM } from "@/lib/utils/cloneFm";
import { ResizableBox } from "react-resizable";
export const TableEditBetter: React.FC<any> = ({ export const TableEditBetter: React.FC<any> = ({
name, name,
column, column,
@ -250,7 +230,14 @@ export const TableEditBetter: React.FC<any> = ({
)} )}
> >
{!disabledHeadTable ? ( {!disabledHeadTable ? (
<thead className="rounded-md overflow-hidden text-md bg-second group/head text-md uppercase text-gray-700 sticky top-0"> <thead
className={cx(
"rounded-md overflow-hidden text-md bg-primary group/head text-md uppercase text-white sticky top-0",
css`
z-index: 1;
`
)}
>
<tr className={"table-header-tbl"}> <tr className={"table-header-tbl"}>
{columns.map((col, idx) => { {columns.map((col, idx) => {
return ( return (
@ -265,7 +252,7 @@ export const TableEditBetter: React.FC<any> = ({
}} }}
> >
<div className="flex items-center h-full flex-grow p-2"> <div className="flex items-center h-full flex-grow p-2">
<span>{col?.name}</span> <span>{col?.header()}</span>
</div> </div>
</th> </th>
); );
@ -277,9 +264,25 @@ export const TableEditBetter: React.FC<any> = ({
)} )}
<tbody> <tbody>
{local.data.map((row: any, index: any) => { {local.data.map((row: any, index: any) => {
const fm_row = cloneFM(fm, row); const fm_row = {
...fm,
data: row,
render: () => {
local.render();
fm.data[name] = local.data;
fm.render();
},
};
return ( return (
<tr key={`row_${name}_${index}`}> <tr
key={`row_${name}_${index}`}
className={cx(css`
td {
vertical-align: ${align};
}
`)}
>
{columns.map((col, idx) => { {columns.map((col, idx) => {
const param = { const param = {
row: row, row: row,
@ -300,7 +303,7 @@ export const TableEditBetter: React.FC<any> = ({
key={`row_${name}_${index}_${col?.accessorKey}_${idx}`} key={`row_${name}_${index}_${col?.accessorKey}_${idx}`}
className={"table-header-tbl capitalize"} className={"table-header-tbl capitalize"}
> >
{renderData} <div className="p-1">{renderData}</div>
</td> </td>
); );
})} })}

View File

@ -15,22 +15,21 @@ import { Button, Label, Table } from "flowbite-react";
import { HiChevronLeft, HiChevronRight, HiPlus } from "react-icons/hi"; import { HiChevronLeft, HiChevronRight, HiPlus } from "react-icons/hi";
import { useLocal } from "@/lib/utils/use-local"; import { useLocal } from "@/lib/utils/use-local";
import { debouncedHandler } from "@/lib/utils/debounceHandler"; import { debouncedHandler } from "@/lib/utils/debounceHandler";
import { FaChevronUp } from "react-icons/fa6"; import { FaSort, FaSortDown, FaSortUp } from "react-icons/fa6";
import Link from "next/link"; import Link from "next/link";
import { init_column } from "./lib/column"; import { init_column } from "./lib/column";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, Sticker } from "lucide-react"; import { Loader2, Sticker } from "lucide-react";
import { InputSearch } from "../ui/input-search"; import { InputSearch } from "../ui/input-search";
import { FaChevronDown } from "react-icons/fa";
import get from "lodash.get"; import get from "lodash.get";
import { Checkbox } from "../ui/checkbox"; import { Checkbox } from "../ui/checkbox";
import { getNumber } from "@/lib/utils/getNumber"; import { getNumber } from "@/lib/utils/getNumber";
import { formatMoney } from "../form/field/TypeInput"; import { formatMoney } from "../form/field/TypeInput";
import { cloneFM } from "@/lib/utils/cloneFm";
export const TableList: React.FC<any> = ({ export const TableList: React.FC<any> = ({
name, name,
column, column,
style = "UI",
align = "center", align = "center",
onLoad, onLoad,
take = 20, take = 20,
@ -149,7 +148,18 @@ export const TableList: React.FC<any> = ({
}); });
const cloneListFM = (data: any[]) => { const cloneListFM = (data: any[]) => {
if (mode === "form") { if (mode === "form") {
local.dataForm = data.map((e: any) => cloneFM(fm, e)); local.dataForm = data.map((e: any) => {
return {
...fm,
data: e,
render: () => {
local.render();
fm.data[name] = local.data;
fm.render();
console.log(fm.data[name]);
},
};
});
local.render(); local.render();
} }
}; };
@ -182,13 +192,18 @@ export const TableList: React.FC<any> = ({
local.render(); local.render();
} }
if (mode === "form") {
local.data = fm.data?.[name] || [];
cloneListFM(fm.data?.[name] || []);
local.render();
setData(fm.data?.[name] || []);
} else {
if (Array.isArray(onLoad)) { if (Array.isArray(onLoad)) {
local.data = onLoad; local.data = onLoad;
cloneListFM(onLoad); cloneListFM(onLoad);
local.render(); local.render();
setData(onLoad); setData(onLoad);
} else { } else if (typeof onLoad === "function") {
const res: any = await onLoad({ const res: any = await onLoad({
search: local.search, search: local.search,
sort: local.sort, sort: local.sort,
@ -199,10 +214,16 @@ export const TableList: React.FC<any> = ({
cloneListFM(res); cloneListFM(res);
local.render(); local.render();
setData(res); setData(res);
} else {
local.data = onLoad;
cloneListFM(onLoad);
local.render();
setData(onLoad);
}
}
setTimeout(() => { setTimeout(() => {
toast.dismiss(); toast.dismiss();
}, 2000); }, 2000);
}
}; };
if (typeof onInit === "function") { if (typeof onInit === "function") {
onInit(local); onInit(local);
@ -337,7 +358,7 @@ export const TableList: React.FC<any> = ({
<> <>
<div className="tbl-wrapper flex flex-grow flex-col"> <div className="tbl-wrapper flex flex-grow flex-col">
{!disabledHeader ? ( {!disabledHeader ? (
<div className="head-tbl-list block items-start justify-between bg-white px-0 py-4 sm:flex"> <div className="head-tbl-list block items-start justify-between px-4 py-4 sm:flex">
<div className="flex flex-row items-end"> <div className="flex flex-row items-end">
<div className="sm:flex flex flex-col space-y-2"> <div className="sm:flex flex flex-col space-y-2">
<div className="flex"> <div className="flex">
@ -399,28 +420,28 @@ export const TableList: React.FC<any> = ({
<div className="relative"> <div className="relative">
<Table <Table
className={cx( className={cx(
"min-w-full divide-y divide-gray-200 ", "min-w-full divide-y divide-gray-200 table-bg",
css` css`
thead th:first-child { // thead th:first-child {
overflow: hidden; // overflow: hidden;
border-top-left-radius: 10px; /* Sudut kiri atas */ // border-top-left-radius: 10px; /* Sudut kiri atas */
border-bottom-left-radius: 10px; // border-bottom-left-radius: 10px;
} // }
thead th:last-child { // thead th:last-child {
overflow: hidden; // overflow: hidden;
border-top-right-radius: 10px; /* Sudut kiri atas */ // border-top-right-radius: 10px; /* Sudut kiri atas */
border-bottom-right-radius: 10px; // border-bottom-right-radius: 10px;
} // }
tbody td:first-child { // tbody td:first-child {
overflow: hidden; // overflow: hidden;
border-top-left-radius: 10px; /* Sudut kiri atas */ // border-top-left-radius: 10px; /* Sudut kiri atas */
border-bottom-left-radius: 10px; // border-bottom-left-radius: 10px;
} // }
tbody td:last-child { // tbody td:last-child {
overflow: hidden; // overflow: hidden;
border-top-right-radius: 10px; /* Sudut kiri atas */ // border-top-right-radius: 10px; /* Sudut kiri atas */
border-bottom-right-radius: 10px; // border-bottom-right-radius: 10px;
} // }
`, `,
checkbox && checkbox &&
css` css`
@ -440,7 +461,14 @@ export const TableList: React.FC<any> = ({
)} )}
> >
{!disabledHeadTable ? ( {!disabledHeadTable ? (
<thead className="rounded-md overflow-hidden text-md bg-second group/head text-md uppercase text-gray-700 sticky top-0"> <thead
className={cx(
"table-head-list overflow-hidden text-md bg-primary text-white group/head text-md uppercase sticky top-0",
css`
z-index: 1;
`
)}
>
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr <tr
key={`${headerGroup.id}`} key={`${headerGroup.id}`}
@ -536,29 +564,38 @@ export const TableList: React.FC<any> = ({
</div> </div>
{isSort ? ( {isSort ? (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<FaChevronUp {local?.sort?.[name] ? (
<>
{local?.sort?.[name] === "asc" ? (
<FaSortUp
className={cx( className={cx(
"px-0.5 mx-1 text-[12px]", "text-xs",
local?.sort?.[name] === "asc" local?.sort?.[name] === "asc"
? "text-black" ? "text-white"
: "text-gray-500" : "text-primary"
)} )}
/> />
<FaChevronDown ) : (
<FaSortDown
className={cx( className={cx(
"px-0.5 mx-1 text-[12px]", "text-xs",
local?.sort?.[name] === "desc" local?.sort?.[name] === "desc"
? "text-black" ? "text-white"
: "text-gray-500" : "text-primary"
)} )}
/> />
)}
</>
) : (
<FaSort className=" text-xs text-white" />
)}
</div> </div>
) : ( ) : (
<></> <></>
)} )}
</div> </div>
{resize &&
{headerGroup.headers.length !== index + 1 ? ( headerGroup.headers.length !== index + 1 ? (
<div <div
key={`${header.id}-resizer`} // Tambahkan key unik key={`${header.id}-resizer`} // Tambahkan key unik
{...{ {...{
@ -567,15 +604,15 @@ export const TableList: React.FC<any> = ({
onMouseDown: header.getResizeHandler(), onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(), onTouchStart: header.getResizeHandler(),
className: cx( className: cx(
`resizer bg-[#b3c9fe] cursor-e-resize ${ `resizer hover:bg-second cursor-e-resize ${
table.options.columnResizeDirection table.options.columnResizeDirection
} ${ } ${
header.column.getIsResizing() header.column.getIsResizing()
? "isResizing" ? "isResizing bg-second"
: "" : " bg-primary"
}`, }`,
css` css`
width: 1px; width: 5px;
cursor: e-resize !important; cursor: e-resize !important;
` `
), ),
@ -608,7 +645,7 @@ export const TableList: React.FC<any> = ({
<></> <></>
)} )}
<Table.Body className="divide-y border-none bg-white"> <Table.Body className="divide-y border-none ">
{table.getRowModel().rows.map((row, idx) => { {table.getRowModel().rows.map((row, idx) => {
const fm_row = const fm_row =
mode === "form" ? local.dataForm?.[idx] : null; mode === "form" ? local.dataForm?.[idx] : null;
@ -623,7 +660,8 @@ export const TableList: React.FC<any> = ({
vertical-align: ${align}; vertical-align: ${align};
} }
`, `,
"border-none" "border-none ",
style === "UI" ? "even:bg-linear-blue " : ""
)} )}
> >
{row.getVisibleCells().map((cell: any) => { {row.getVisibleCells().map((cell: any) => {
@ -737,7 +775,7 @@ export const Pagination: React.FC<any> = ({
local.render(); local.render();
}, [page, count]); }, [page, count]);
return ( return (
<div className=" border-t border-gray-300 tbl-pagination sticky text-sm bottom-0 right-0 w-full grid grid-cols-3 gap-4 justify-end text-sm bg-white pt-2"> <div className=" border-t border-gray-300 p-2 tbl-pagination sticky text-sm bottom-0 right-0 w-full grid grid-cols-3 gap-4 justify-end text-sm ">
<div className="flex flex-row items-center text-gray-600"> <div className="flex flex-row items-center text-gray-600">
Showing {local.page * 20 - 19} to{" "} Showing {local.page * 20 - 19} to{" "}
{list.data?.length >= 20 {list.data?.length >= 20
@ -843,7 +881,7 @@ export const PaginationPage: React.FC<any> = ({
local.render(); local.render();
}, [page, count]); }, [page, count]);
return ( return (
<div className=" tbl-pagination text-sm bottom-0 right-0 w-full grid grid-cols-1 gap-4 justify-center text-sm bg-white pt-2"> <div className=" tbl-pagination text-sm bottom-0 right-0 w-full grid grid-cols-1 gap-4 justify-center text-sm pt-2">
<div className="flex flex-row items-center justify-center"> <div className="flex flex-row items-center justify-center">
<div className="flex items-center flex-row gap-x-2 sm:mb-0 text-sm"> <div className="flex items-center flex-row gap-x-2 sm:mb-0 text-sm">
<div <div

View File

@ -0,0 +1,962 @@
"use client";
import {
ColumnDef,
ColumnResizeDirection,
ColumnResizeMode,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { useCallback, useEffect, useState } from "react";
import { Button, Label, Table } from "flowbite-react";
import { HiChevronLeft, HiChevronRight, HiPlus } from "react-icons/hi";
import { useLocal } from "@/lib/utils/use-local";
import { debouncedHandler } from "@/lib/utils/debounceHandler";
import { FaSort, FaSortDown, FaSortUp } from "react-icons/fa6";
import Link from "next/link";
import { init_column } from "./lib/column";
import { toast } from "sonner";
import { Loader2, Sticker } from "lucide-react";
import { InputSearch } from "../ui/input-search";
import get from "lodash.get";
import { Checkbox } from "../ui/checkbox";
import { getNumber } from "@/lib/utils/getNumber";
import { formatMoney } from "../form/field/TypeInput";
import { cloneFM } from "@/lib/utils/cloneFm";
export const TableListBetter: React.FC<any> = ({
name,
column,
align = "center",
onLoad,
take = 20,
header,
disabledPagination,
disabledHeader,
disabledHeadTable,
hiddenNoRow,
disabledHoverRow,
onInit,
onCount,
fm,
mode,
feature,
onChange,
}) => {
const [data, setData] = useState<any[]>([]);
const sideLeft =
typeof header?.sideLeft === "function" ? header.sideLeft : null;
const sideRight =
typeof header?.sideRight === "function" ? header.sideRight : null;
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
};
const checkbox =
Array.isArray(feature) && feature?.length
? feature.includes("checkbox")
: false;
const local = useLocal({
table: null as any,
data: [] as any[],
dataForm: [] as any[],
listData: [] as any[],
sort: {} as any,
search: null as any,
count: 0 as any,
addRow: (row: any) => {
setData((prev) => [...prev, row]);
local.data.push(row);
local.render();
},
selection: {
all: false,
partial: [] as any[],
},
renderRow: (row: any) => {
setData((prev) => [...prev, row]);
local.data = data;
local.render();
},
removeRow: (row: any) => {
setData((prev) => prev.filter((item) => item !== row)); // Update state lokal
local.data = local.data.filter((item: any) => item !== row); // Hapus row dari local.data
local.render(); // Panggil render untuk memperbarui UI
},
reload: async () => {
toast.info(
<>
<Loader2
className={cx(
"h-4 w-4 animate-spin-important",
css`
animation: spin 1s linear infinite !important;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`
)}
/>
{"Loading..."}
</>
);
if (Array.isArray(onLoad)) {
local.data = onLoad;
local.render();
setData(onLoad);
} else {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
cloneListFM(e);
local.render();
setData(e);
setTimeout(() => {
toast.dismiss();
}, 2000);
});
} else {
local.data = res;
cloneListFM(res);
local.render();
setData(res);
setTimeout(() => {
toast.dismiss();
}, 2000);
}
}
},
});
const cloneListFM = (data: any[]) => {
if (mode === "form") {
local.dataForm = data.map((e: any) => cloneFM(fm, e));
local.render();
}
};
useEffect(() => {
const run = async () => {
toast.info(
<>
<Loader2
className={cx(
"h-4 w-4 animate-spin-important",
css`
animation: spin 1s linear infinite !important;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
`
)}
/>
{"Loading..."}
</>
);
if (typeof onCount === "function") {
const res = await onCount();
local.count = res;
local.render();
}
if (Array.isArray(onLoad)) {
local.data = onLoad;
cloneListFM(onLoad);
local.render();
setData(onLoad);
} else {
const res: any = await onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
local.data = res;
cloneListFM(res);
local.render();
setData(res);
setTimeout(() => {
toast.dismiss();
}, 2000);
}
};
if (typeof onInit === "function") {
onInit(local);
}
run();
}, []);
const defaultColumns: ColumnDef<Person>[] = init_column(column);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columns] = React.useState<typeof defaultColumns>(() =>
checkbox
? [
{
id: "select",
width: 10,
header: ({ table }) => (
<Checkbox
id="terms"
checked={table.getIsAllRowsSelected()}
onClick={(e) => {
table.getToggleAllRowsSelectedHandler();
const handler = table.getToggleAllRowsSelectedHandler();
handler(e); // Pastikan ini memanggil fungsi handler yang benar
local.selection.all = !local.selection.all;
local.render();
}}
/>
),
cell: ({ row }) => {
const findCheck = (row: any) => {
if (row.getIsSelected()) return true;
const data: any = row.original;
const res = local.selection.partial.find((e) => e === data?.id);
return res ? true : false;
};
return (
<div className="px-0.5 items-center justify-center flex flex-row">
<Checkbox
id="terms"
checked={findCheck(row)}
onClick={(e) => {
const handler = row.getToggleSelectedHandler();
handler(e); // Pastikan ini memanggil fungsi handler yang benar
const data: any = row.original;
const checked = local.selection.all
? true
: local.selection.partial.find((e) => e === data?.id);
if (!checked) {
local.selection.partial.push(data?.id);
} else {
if (
local.selection.partial.find((e) => e === data?.id)
) {
local.selection.partial =
local.selection.partial.filter(
(e: any) => e !== data?.id
);
}
local.selection.all = false;
}
local.render();
}}
/>
</div>
);
},
sortable: false,
},
...defaultColumns,
]
: [...defaultColumns]
);
const [columnResizeMode, setColumnResizeMode] =
React.useState<ColumnResizeMode>("onChange");
const [columnResizeDirection, setColumnResizeDirection] =
React.useState<ColumnResizeDirection>("ltr");
// Create the table and pass your options
useEffect(() => {
setData(local.data);
}, [local.data.length]);
const paginationConfig = disabledPagination
? {}
: {
getPaginationRowModel: getPaginationRowModel(),
};
const [pagination, setPagination] = React.useState({
pageIndex: 0,
pageSize: 20,
});
const table = useReactTable({
data: data,
columnResizeMode,
pageCount: Math.ceil(local.count / 20),
manualPagination: true,
columnResizeDirection,
columns,
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
initialState: {
pagination: {
pageIndex: 0,
pageSize: 20, //custom default page size
},
},
state: {
pagination,
sorting,
},
...paginationConfig,
});
local.table = table;
// Manage your own state
const [state, setState] = React.useState(table.initialState);
// Override the state managers for the table to your own
table.setOptions((prev) => ({
...prev,
state,
onStateChange: setState,
debugTable: state.pagination.pageIndex > 2,
}));
const handleSearch = useCallback(
debouncedHandler(() => {
local.reload();
}, 1000),
[]
);
return (
<>
<div className="tbl-wrapper flex flex-grow flex-col">
{!disabledHeader ? (
<div className="head-tbl-list block items-start justify-between px-4 py-4 sm:flex">
<div className="flex flex-row items-end">
<div className="sm:flex flex flex-col space-y-2">
<div className="flex">
{sideLeft ? (
sideLeft(local)
) : (
<>
<Link href={"/new"}>
<Button className="bg-primary">
<div className="flex items-center gap-x-0.5">
<HiPlus className="text-xl" />
<span className="capitalize">Add {name}</span>
</div>
</Button>
</Link>
</>
)}
</div>
</div>
</div>
<div className="ml-auto flex items-center flex-row">
<div className="tbl-search hidden items-center sm:mb-0 sm:flex sm:divide-x sm:divide-gray-100">
<form
onSubmit={async (e) => {
e.preventDefault();
await local.reload();
}}
>
<Label htmlFor="users-search" className="sr-only">
Search
</Label>
<div className="relative lg:w-56">
<InputSearch
// className="bg-white search text-xs "
id="users-search"
name="users-search"
placeholder={`Search`}
onChange={(e) => {
const value = e.target.value;
local.search = value;
local.render();
handleSearch();
}}
/>
</div>
</form>
</div>
<div className="flex">{sideRight ? sideRight(local) : <></>}</div>
</div>
</div>
) : (
<></>
)}
<div className="flex flex-col flex-grow">
<div className="container-table overflow-auto relative flex-grow flex-row">
<div className="tbl absolute top-0 left-0 inline-block flex-grow w-full h-full align-middle">
<div className="relative">
<Table
className={cx(
"min-w-full divide-y divide-gray-200 table-bg",
css`
// thead th:first-child {
// overflow: hidden;
// border-top-left-radius: 10px; /* Sudut kiri atas */
// border-bottom-left-radius: 10px;
// }
// thead th:last-child {
// overflow: hidden;
// border-top-right-radius: 10px; /* Sudut kiri atas */
// border-bottom-right-radius: 10px;
// }
// tbody td:first-child {
// overflow: hidden;
// border-top-left-radius: 10px; /* Sudut kiri atas */
// border-bottom-left-radius: 10px;
// }
// tbody td:last-child {
// overflow: hidden;
// border-top-right-radius: 10px; /* Sudut kiri atas */
// border-bottom-right-radius: 10px;
// }
`,
checkbox &&
css`
.table-header-tbl > th:first-child {
width: 20px !important; /* Atur lebar sesuai kebutuhan */
text-align: center;
min-width: 40px;
max-width: 40px;
}
.table-row-element > td:first-child {
width: 20px !important; /* Atur lebar sesuai kebutuhan */
text-align: center;
min-width: 40px;
max-width: 40px;
}
`
)}
>
{!disabledHeadTable ? (
<thead className="table-head-list overflow-hidden text-md bg-primary text-white group/head text-md uppercase sticky top-0">
{table.getHeaderGroups().map((headerGroup) => (
<tr
key={`${headerGroup.id}`}
className={"table-header-tbl"}
>
{headerGroup.headers.map((header, index) => {
const name = header.column.id;
const col = column.find(
(e: any) => e?.name === name
);
const isSort =
name === "select"
? false
: typeof col?.sortable === "boolean"
? col.sortable
: true;
const resize =
name === "select"
? false
: typeof col?.resize === "boolean"
? col.resize
: true;
return (
<th
{...{
style: {
width: !resize
? `${col?.width}px`
: name === "select"
? `${5}px`
: col?.width
? header.getSize() < col?.width
? `${col.width}px`
: header.getSize()
: header.getSize(),
},
}}
key={header.id}
colSpan={header.colSpan}
className={cx(
"relative px-2 py-2 text-sm py-1 uppercase",
name === "select" &&
css`
max-width: 5px;
`
)}
>
<div
key={`${header.id}-label`}
{...{
style: col?.width
? {
minWidth: `${col.width}px`,
}
: {},
}}
onClick={() => {
if (isSort) {
const sort = local?.sort?.[name];
const mode =
sort === "desc"
? null
: sort === "asc"
? "desc"
: "asc";
local.sort = mode
? {
[name]: mode,
}
: {};
local.render();
local.reload();
}
}}
className={cx(
"flex flex-grow flex-row flex-grow select-none items-center flex-row text-base text-nowrap",
isSort ? " cursor-pointer" : ""
)}
>
<div
className={cx(
"flex flex-row items-center flex-grow text-sm capitalize",
name === "select" ? "justify-center" : ""
)}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</div>
{isSort ? (
<div className="flex flex-col items-center">
{local?.sort?.[name] ? (
<>
{local?.sort?.[name] === "asc" ? (
<FaSortUp
className={cx(
"text-xs",
local?.sort?.[name] === "asc"
? "text-white"
: "text-primary"
)}
/>
) : (
<FaSortDown
className={cx(
"text-xs",
local?.sort?.[name] === "desc"
? "text-white"
: "text-primary"
)}
/>
)}
</>
) : (
<FaSort className=" text-xs text-white" />
)}
</div>
) : (
<></>
)}
</div>
{resize &&
headerGroup.headers.length !== index + 1 ? (
<div
key={`${header.id}-resizer`} // Tambahkan key unik
{...{
onDoubleClick: () =>
header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
className: cx(
`resizer hover:bg-second cursor-e-resize ${
table.options.columnResizeDirection
} ${
header.column.getIsResizing()
? "isResizing bg-second"
: " bg-primary"
}`,
css`
width: 1px;
cursor: e-resize !important;
`
),
style: {
transform:
columnResizeMode === "onEnd" &&
header.column.getIsResizing()
? `translateX(${
(table.options
.columnResizeDirection ===
"rtl"
? -1
: 1) *
(table.getState()
.columnSizingInfo
.deltaOffset ?? 0)
}px)`
: "",
},
}}
></div>
) : null}
</th>
);
})}
</tr>
))}
</thead>
) : (
<></>
)}
<Table.Body className="divide-y border-none ">
{table.getRowModel().rows.map((row, idx) => {
const fm_row =
mode === "form" ? local.dataForm?.[idx] : null;
return (
<Table.Row
key={row.id}
className={cx(
disabledHoverRow ? "" : "hover:bg-gray-100",
css`
height: 44px;
> td {
vertical-align: ${align};
}
`,
"border-none even:bg-linear-blue "
)}
>
{row.getVisibleCells().map((cell: any) => {
const ctx = cell.getContext();
const param = {
row: row.original,
name: get(ctx, "column.columnDef.accessorKey"),
cell,
idx,
tbl: local,
fm_row: fm_row,
onChange,
};
const head = column.find(
(e: any) =>
e?.name ===
get(ctx, "column.columnDef.accessorKey")
);
const renderData =
typeof head?.renderCell === "function"
? head.renderCell(param)
: flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
return (
<Table.Cell
className={cx(
"text-md px-2 py-1 whitespace-nowrap text-gray-900 items-start",
name === "select"
? css`
width: 5px;
`
: ``
)}
key={cell.id}
>
{renderData}
</Table.Cell>
);
})}
</Table.Row>
);
})}
</Table.Body>
</Table>
</div>
</div>
{!hiddenNoRow && !table.getRowModel().rows?.length && (
<div
className={cx(
"flex-1 w-full absolute inset-0 flex flex-col items-center justify-center",
css`
top: 50%;
transform: translateY(-50%);
`
)}
>
<div className="max-w-[15%] flex flex-col items-center">
<Sticker size={35} strokeWidth={1} />
<div className="pt-1 text-center">No&nbsp;Data</div>
</div>
</div>
)}
</div>
</div>
<PaginationPage
list={local}
count={local.count}
onNextPage={() => table.nextPage()}
onPrevPage={() => table.previousPage()}
disabledNextPage={!table.getCanNextPage()}
disabledPrevPage={!table.getCanPreviousPage()}
page={table.getState().pagination.pageIndex + 1}
setPage={(page: any) => {
setPagination({
pageIndex: page,
pageSize: 20,
});
}}
countPage={table.getPageCount()}
countData={local.data.length}
take={take}
onChangePage={(page: number) => {
table.setPageIndex(page);
}}
/>
</div>
</>
);
};
export const Pagination: React.FC<any> = ({
onNextPage,
onPrevPage,
disabledNextPage,
disabledPrevPage,
page,
count,
list,
setPage,
onChangePage,
}) => {
const local = useLocal({
page: 1 as any,
pagination: [] as any,
});
useEffect(() => {
local.page = page;
local.pagination = getPagination(page, Math.ceil(count / 20));
local.render();
}, [page, count]);
return (
<div className=" border-t border-gray-300 tbl-pagination sticky text-sm bottom-0 right-0 w-full grid grid-cols-3 gap-4 justify-end text-sm pt-2">
<div className="flex flex-row items-center text-gray-600">
Showing {local.page * 20 - 19} to{" "}
{list.data?.length >= 20
? local.page * 20
: local.page === 1 && Math.ceil(count / 20) === 1
? list.data?.length
: local.page * 20 - 19 + list.data?.length}{" "}
of {formatMoney(getNumber(count))} results
</div>
<div className="flex flex-row justify-center">
<div>
<nav
className="isolate inline-flex -space-x-px flex flex-row items-center gap-x-2"
aria-label="Pagination"
>
{local.pagination.map((e: any, idx: number) => {
return (
<div
key={"page_" + idx}
onClick={() => {
if (e?.label !== "...") {
local.page = getNumber(e?.label);
local.render();
onChangePage(local.page - 1);
setPage(local.page - 1);
list.reload();
}
}}
className={cx(
"text-sm px-2 py-1",
e.active
? "relative z-10 inline-flex items-center bg-primary font-semibold text-white rounded-md"
: e?.label === "..."
? "relative z-10 inline-flex items-center font-semibold text-gray-800 rounded-md"
: "cursor-pointer relative z-10 inline-flex items-center hover:bg-gray-100 font-semibold text-gray-800 rounded-md"
)}
>
{e?.label}
</div>
);
})}
</nav>
</div>
</div>
<div className="flex flex-row items-center justify-end">
<div className="flex items-center flex-row gap-x-2 sm:mb-0 text-sm">
<div
onClick={() => {
if (!disabledPrevPage) {
onPrevPage();
}
}}
className={cx(
"flex flex-row items-center gap-x-2 justify-center rounded p-1 ",
disabledPrevPage
? "text-gray-200 border-gray-200 border px-2"
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2"
)}
>
<HiChevronLeft className="text-sm" />
{/* <span>Previous</span> */}
</div>
<div
onClick={() => {
if (!disabledNextPage) {
onNextPage();
}
}}
className={cx(
"flex flex-row items-center gap-x-2 justify-center rounded p-1 ",
disabledNextPage
? "text-gray-200 border-gray-200 border px-2"
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2"
)}
>
{/* <span>Next</span> */}
<HiChevronRight className="text-sm" />
</div>
</div>
</div>
</div>
);
};
export const PaginationPage: React.FC<any> = ({
onNextPage,
onPrevPage,
disabledNextPage,
disabledPrevPage,
page,
count,
list,
take,
setPage,
onChangePage,
}) => {
const local = useLocal({
page: 1 as any,
pagination: [] as any,
});
useEffect(() => {
local.page = page;
local.pagination = getPagination(page, Math.ceil(count / take));
local.render();
}, [page, count]);
return (
<div className="py-2 tbl-pagination text-sm bottom-0 right-0 w-full grid grid-cols-1 gap-4 justify-center text-sm">
<div className="flex flex-row items-center justify-center">
<div className="flex items-center flex-row gap-x-2 sm:mb-0 text-sm">
<div
onClick={() => {
if (!disabledPrevPage) {
onPrevPage();
}
}}
className={cx(
"flex flex-row items-center gap-x-2 justify-center rounded-full p-2 text-md",
disabledPrevPage
? "text-gray-200 border-gray-200 border "
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border "
)}
>
<HiChevronLeft />
</div>
<div className="flex flex-row justify-center">
<div>
<nav
className="isolate inline-flex -space-x-px flex flex-row items-center gap-x-2"
aria-label="Pagination"
>
{local.pagination.map((e: any, idx: number) => {
return (
<div
key={"page_" + idx}
onClick={() => {
if (e?.label !== "...") {
local.page = getNumber(e?.label);
local.render();
onChangePage(local.page - 1);
setPage(local.page - 1);
}
}}
className={cx(
"text-md px-2.5 py-1",
e.active
? "relative z-10 inline-flex items-center bg-primary font-semibold text-white rounded-full"
: e?.label === "..."
? "relative z-10 inline-flex items-center font-semibold text-gray-800 rounded-full"
: "cursor-pointer relative z-10 inline-flex items-center hover:bg-gray-100 font-semibold text-gray-800 rounded-full"
)}
>
{e?.label}
</div>
);
})}
</nav>
</div>
</div>
<div
onClick={() => {
if (!disabledNextPage) {
onNextPage();
}
}}
className={cx(
"flex flex-row items-center gap-x-2 justify-center rounded-full p-2 ",
disabledNextPage
? "text-gray-200 border-gray-200 border"
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border "
)}
>
<HiChevronRight className="text-md" />
</div>
</div>
</div>
</div>
);
};
const getPagination = (currentPage: number, totalPages: number) => {
const pagination: { label: string; active: boolean }[] = [];
const maxVisible = 5; // Jumlah maksimal elemen yang ditampilkan
const halfRange = Math.floor((maxVisible - 3) / 2);
if (totalPages <= maxVisible) {
// Jika total halaman lebih kecil dari batas, tampilkan semua halaman
for (let i = 1; i <= totalPages; i++) {
pagination.push({ label: i.toString(), active: i === currentPage });
}
} else {
pagination.push({ label: "1", active: currentPage === 1 }); // Halaman pertama selalu ada
if (currentPage > halfRange + 2) {
pagination.push({ label: "...", active: false }); // Awal titik-titik
}
const startPage = Math.max(2, currentPage - halfRange);
const endPage = Math.min(totalPages - 1, currentPage + halfRange);
for (let i = startPage; i <= endPage; i++) {
pagination.push({ label: i.toString(), active: i === currentPage });
}
if (currentPage < totalPages - halfRange - 1) {
pagination.push({ label: "...", active: false }); // Akhir titik-titik
}
pagination.push({
label: totalPages.toString(),
active: currentPage === totalPages,
}); // Halaman terakhir selalu ada
}
return pagination;
};

View File

@ -0,0 +1,98 @@
"use client";
import React from "react";
import { TableList } from "./TableList";
import { useLocal } from "@/lib/utils/use-local";
import get from "lodash.get";
import { TabHeaderBetter } from "../tablist/TabHeaderBetter";
import { getNumber } from "@/lib/utils/getNumber";
export const TableUI: React.FC<any> = ({
name,
column,
align = "center",
onLoad,
take = 20,
header,
disabledPagination,
disabledHeader,
disabledHeadTable,
hiddenNoRow,
disabledHoverRow,
onInit,
onCount,
fm,
mode,
feature,
onChange,
delete_name,
title,
tab,
onTab,
}) => {
const local = useLocal({
tab: get(tab, "[0].id"),
});
return (
<div className="flex flex-col flex-grow">
<div className="w-full flex flex-row p-4 py-6 pr-6 pl-3 text-2xl font-bold">
{title}
</div>
<div className="flex flex-col flex-grow bg-card-layer rounded-lg border border-gray-300 shadow-md shadow-gray-300 overflow-hidden">
<div className="flex flex-col flex-grow">
{tab?.length && (
<div className="flex flex-row justify-start">
<div className="flex flex-row">
<TabHeaderBetter
disabledPagination={true}
onLabel={(row: any) => {
console.log({ row });
return (
<div className="flex flex-row items-center gap-x-2 font-bold">
<div className="text-3xl">
{getNumber(row?.count) > 999
? "99+"
: getNumber(row?.count)}
</div>
<div className="flex flex-col justify-start ">
<div className="text-start">Total</div>
<div>{row.name}</div>
</div>
</div>
);
}}
onValue={(row: any) => {
return row.id;
}}
onLoad={tab}
onChange={(tab: any) => {
local.tab = tab?.id;
local.render();
if (typeof onTab === "function") {
onTab(local.tab);
}
}}
tabContent={(data: any) => {
return <></>;
}}
/>
</div>
</div>
)}
<div className="flex flex-col bg-white mt-[-1px] flex-grow">
<div className="w-full flex flex-row flex-grow overflow-hidden ">
<TableList
name={name}
header={header}
column={column}
onLoad={onLoad}
onCount={onCount}
onInit={onInit}
/>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,91 @@
"use client";
import React, { useEffect } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { useLocal } from "@/lib/utils/use-local";
export const TabHeaderBetter: React.FC<any> = ({
name,
column,
onLabel,
onValue,
onLoad,
take = 20,
header,
tabContent,
disabledPagination,
onChange,
}) => {
const sideRight =
typeof header?.sideRight === "function" ? header.sideRight : null;
const local = useLocal({
data: [] as any[],
sort: {} as any,
search: null as any,
reload: async () => {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
local.render();
});
} else {
local.data = res;
local.render();
}
},
});
useEffect(() => {
local.data = onLoad;
local.render();
}, []);
if (!local.data?.length) return <></>;
return (
<div className="flex flex-row w-full">
<Tabs
className="flex flex-col w-full"
defaultValue={onValue(local.data?.[0])}
>
<TabsList className="flex flex-row relative w-full bg-transparent p-0 rounded-none">
{Array.isArray(onLoad) &&
onLoad.map((e, idx) => {
return (
<TabsTrigger
value={onValue(e)}
onClick={() => {
if (typeof onChange === "function") {
onChange(e);
}
}}
className={cx(
"p-1.5 px-4 border text-sm text-gray-500 border-none mr-0 rounded-none focus-visible:ring-0 data-[state=active]:ring-0 transition-none bg-card-layer data-[state=active]:bg-white data-[state=active]:text-primary data-[state=active]:shadow-none data-[state=active]:border-none",
idx === 0
? "data-[state=active]:slanted-edge data-[state=active]:pr-8 rounded-tl-sm"
: "data-[state=active]:parallelogram pr-8 data-[state=active]:pl-8",
!idx ? "" : idx++ === local.data.length ? "" : ""
)}
key={onValue(e)}
>
{onLabel(e)}
<div className="triangle"></div>
</TabsTrigger>
);
})}
</TabsList>
{local.data.map((e) => {
return (
<TabsContent value={onValue(e)} key={onValue(e) + "_tabcontent"}>
<div className="flex flex-row flex-grow w-full h-full">
{tabContent(e)}
</div>
</TabsContent>
);
})}
</Tabs>
</div>
);
};

View File

@ -0,0 +1,27 @@
"use client";
import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import { cn } from "@/lib/utils/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };

View File

@ -0,0 +1,47 @@
"use client";
import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
import { cn } from "@/lib/utils/utils";
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit] relative">
<div className="absolute top-0 left-0 w-full h-full">{children}</div>
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"hover:bg-gray-100/50 flex touch-none select-none transition-colors ",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent ",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent ",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border bg-gray-300" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
export { ScrollArea, ScrollBar };

View File

@ -0,0 +1,20 @@
import React from "react";
export default function CustomScroll({ children, className }: any) {
return (
<div className={`relative ${className}`}>
{/* Kontainer Scroll */}
<div className="relative w-full h-full overflow-hidden rounded-lg">
<div className="scroll-area w-full h-full overflow-y-auto">
{/* Konten Scroll */}
{children}
</div>
</div>
{/* Scrollbar Manual */}
<div className="scrollbar-track absolute top-0 right-0 w-2 h-full bg-gray-200 rounded-lg">
<div className="scrollbar-thumb w-2 bg-gray-500 rounded-lg transition-transform duration-200 hover:bg-gray-600"></div>
</div>
</div>
);
}

31
components/ui/tooltip.tsx Normal file
View File

@ -0,0 +1,31 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -10,8 +10,11 @@
"@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-progress": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.7",
"@react-pdf/renderer": "^4.1.5", "@react-pdf/renderer": "^4.1.5",
"@tanstack/react-table": "^8.20.5", "@tanstack/react-table": "^8.20.5",
"@tiptap/extension-color": "^2.11.2", "@tiptap/extension-color": "^2.11.2",