From 2c1acf200794984fd4e39ce516e65f9a46181378 Mon Sep 17 00:00:00 2001 From: faisolavolut Date: Fri, 31 Jan 2025 13:43:28 +0700 Subject: [PATCH] update 11 files --- components/form/Field.tsx | 12 +- components/form/FormBetter.tsx | 75 +- components/form/field/FilePreview.tsx | 19 +- components/form/field/TypeInput.tsx | 27 +- components/form/field/TypeRichText.tsx | 3 +- components/form/field/TypeUploadSingle.tsx | 5 +- components/partials/Sidebar.tsx | 31 +- components/partials/SidebarBetter.tsx | 775 +++++++++++++++++ components/tablelist/TableBetter.tsx | 59 +- components/tablelist/TableList.tsx | 180 ++-- components/tablelist/TableListBetter.tsx | 962 +++++++++++++++++++++ components/tablelist/TableUI.tsx | 98 +++ components/tablist/TabHeaderBetter.tsx | 91 ++ components/ui/Progress.tsx | 27 + components/ui/scroll-area.tsx | 47 + components/ui/scroll-better.tsx | 20 + components/ui/tooltip.tsx | 31 + package.json | 3 + 18 files changed, 2303 insertions(+), 162 deletions(-) create mode 100644 components/partials/SidebarBetter.tsx create mode 100644 components/tablelist/TableListBetter.tsx create mode 100644 components/tablelist/TableUI.tsx create mode 100644 components/tablist/TabHeaderBetter.tsx create mode 100644 components/ui/Progress.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/scroll-better.tsx create mode 100644 components/ui/tooltip.tsx diff --git a/components/form/Field.tsx b/components/form/Field.tsx index 3c17962..bbe7732 100644 --- a/components/form/Field.tsx +++ b/components/form/Field.tsx @@ -3,7 +3,6 @@ import { FieldCheckbox } from "./field/TypeCheckbox"; import { TypeDropdown } from "./field/TypeDropdown"; import { TypeInput } from "./field/TypeInput"; import { TypeUpload } from "./field/TypeUpload"; -import { FieldUploadMulti } from "./field/TypeUploadMulti"; import { TypeRichText } from "./field/TypeRichText"; import { TypeTag } from "./field/TypeTag"; import get from "lodash.get"; @@ -23,6 +22,7 @@ export const Field: React.FC = ({ hidden_label, onChange, className, + classField, style, prefix, suffix, @@ -98,11 +98,16 @@ export const Field: React.FC = ({ {!hidden_label ? ( ) : ( <> @@ -137,7 +142,8 @@ export const Field: React.FC = ({ ["upload"].includes(type) && css` padding: 0px !important; - ` + `, + classField )} > {before && ( diff --git a/components/form/FormBetter.tsx b/components/form/FormBetter.tsx index af04dc3..8d245a2 100644 --- a/components/form/FormBetter.tsx +++ b/components/form/FormBetter.tsx @@ -1,4 +1,5 @@ "use client"; +import { ScrollArea } from "../ui/scroll-area"; import { Form } from "./Form"; import { useEffect, useState } from "react"; @@ -21,50 +22,52 @@ export const FormBetter: React.FC = ({ return (
{typeof fm === "object" && typeof onTitle === "function" ? ( -
+
{onTitle(fm)}
) : ( <> )} -
-
-
{ - setFM(form); +
+
+ + { + setFM(form); - const originalRender = form.render; + const originalRender = form.render; - // Buat versi baru dari `local.render` - form.render = () => { - // Panggil fungsi asli - originalRender(); + // Buat versi baru dari `local.render` + form.render = () => { + // Panggil fungsi asli + originalRender(); - // Tambahkan logika tambahan untuk sinkronisasi - setFM({ - ...form, - submit: form.submit, - render: form.render, - data: form.data, - }); - }; - form.render(); - if (typeof onInit === "function") { - onInit(form); - } - }, - }} - /> + // Tambahkan logika tambahan untuk sinkronisasi + setFM({ + ...form, + submit: form.submit, + render: form.render, + data: form.data, + }); + }; + form.render(); + if (typeof onInit === "function") { + onInit(form); + } + }, + }} + /> +
diff --git a/components/form/field/FilePreview.tsx b/components/form/field/FilePreview.tsx index e5db92c..7c35603 100644 --- a/components/form/field/FilePreview.tsx +++ b/components/form/field/FilePreview.tsx @@ -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; if (url instanceof File) { ural = `${URL.createObjectURL(url)}.${url.name.split(".").pop()}`; @@ -211,7 +217,7 @@ export const FilePreview = ({ url }: { url: any }) => { {file.extension && (
{ // border-bottom: 1px solid #1c4ed8; // outline: 1px solid #1c4ed8; } - ` + `, + disabled ? "bg-transparent" : "bg-white" )} onClick={() => { let _url: any = @@ -231,8 +238,10 @@ export const FilePreview = ({ url }: { url: any }) => { window.open(_url, "_blank"); }} > -
{content}
-
{file?.name}
+
+
{content}
+
{file?.name}
+
diff --git a/components/form/field/TypeInput.tsx b/components/form/field/TypeInput.tsx index b84f37c..5dba222 100644 --- a/components/form/field/TypeInput.tsx +++ b/components/form/field/TypeInput.tsx @@ -5,7 +5,7 @@ import { Textarea } from "../../ui/text-area"; import { useEffect, useRef, useState } from "react"; import tinycolor from "tinycolor2"; 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 { getNumber } from "@/lib/utils/getNumber"; import MaskedInput from "../../ui/MaskedInput"; @@ -34,6 +34,7 @@ export const TypeInput: React.FC = ({ const input = useLocal({ value: 0 as any, ref: null as any, + show_pass: false as boolean, open: false, }); const meta = useLocal({ @@ -319,6 +320,10 @@ export const TypeInput: React.FC = ({ ); break; } + let type_field = type; + if (input.show_pass) { + type_field = "text"; + } return ( <> = ({ required={required} placeholder={placeholder || ""} value={value} - type={!type ? "text" : type} + type={!type ? "text" : type_field} onChange={(ev) => { fm.data[name] = ev.currentTarget.value; fm.render(); @@ -350,6 +355,24 @@ export const TypeInput: React.FC = ({ } }} /> + + {type === "password" && ( +
{ + input.show_pass = !input.show_pass; + input.render(); + }} + > +
+ {input.show_pass ? ( + + ) : ( + + )} +
+
+ )} ); }; diff --git a/components/form/field/TypeRichText.tsx b/components/form/field/TypeRichText.tsx index 062dce1..fa6be7f 100644 --- a/components/form/field/TypeRichText.tsx +++ b/components/form/field/TypeRichText.tsx @@ -18,7 +18,6 @@ import TableCell from "@tiptap/extension-table-cell"; import TableHeader from "@tiptap/extension-table-header"; import TableRow from "@tiptap/extension-table-row"; import { ButtonRichText } from "../../ui/button-rich-text"; -import { InitEditor } from "./AfterEditor"; export const TypeRichText: React.FC = ({ name, @@ -822,7 +821,7 @@ export const TypeRichText: React.FC = ({ onChange(fm.data[name]); } }} - content={input.value} + content={fm.data?.[name]} editable={!disabled} >
diff --git a/components/form/field/TypeUploadSingle.tsx b/components/form/field/TypeUploadSingle.tsx index fe45c81..a238c48 100644 --- a/components/form/field/TypeUploadSingle.tsx +++ b/components/form/field/TypeUploadSingle.tsx @@ -199,7 +199,10 @@ export const FieldUploadSingle: FC<{
) : input.fase === "preview" ? (
- + {!disabled ? ( <>
= ({ data, minimaze, mini }) => { div { @@ -332,20 +333,22 @@ const SidebarTree: React.FC = ({ data, minimaze, mini }) => { ` )} > -
-
- - - {renderTree(data)} - - + +
+
+ + + {renderTree(data)} + + +
-
+
diff --git a/components/partials/SidebarBetter.tsx b/components/partials/SidebarBetter.tsx new file mode 100644 index 0000000..12e50cd --- /dev/null +++ b/components/partials/SidebarBetter.tsx @@ -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 = ({ + 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 ( + + {hasChildren ? ( +
  • +
    +
    + {mini && !depth ? ( + <> + + + +
    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} + > +
    + {!depth ? ( +
    + {item.icon} +
    + ) : ( + <> + )} + + {!mini ? ( + <> +
    + {item.title} +
    +
    + {isOpen ? ( + + ) : ( + + )} +
    + + ) : ( + <> + )} +
    +
    +
    + +

    {item.title}

    +
    +
    +
    + + ) : ( + <> +
    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} + > +
    + {!depth ? ( +
    + {item.icon} +
    + ) : ( + <> + )} + + {!mini ? ( + <> +
    + {item.title} +
    +
    + {isOpen ? : } +
    + + ) : ( + <> + )} +
    +
    + + )} +
    +
    + + + {renderTree(item.children!, depth + 1)} + +
  • + ) : ( +
  • +
    +
    + {mini ? ( + + + +
    + { + 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 + > +
    + {!depth ? ( +
    + {item.icon} +
    + ) : ( + <> + )} + {!mini ? ( + <> +
    + {item.title} +
    + + ) : ( + <> + )} +
    +
    +
    +
    + +

    {item.title}

    +
    +
    +
    + ) : ( + { + 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 + > +
    + {!depth ? ( +
    + {item.icon} +
    + ) : ( + <> + )} + {!mini ? ( + <> +
    + {item.title} +
    + + ) : ( + <> + )} +
    +
    + )} +
    +
    +
  • + )} +
    + ); + }); + }; + + return ( +
    +
    { + minimaze(); + localStorage.setItem("mini", !mini ? "true" : "false"); + }} + > + +
    +
    + div { + border-radius: 0px; + background: transparent; + padding-top: 0; + padding-right: 0; + padding-left: 0; + padding-bottom: 0; + } + ` + )} + > + +
    +
    + + + {renderTree(data)} + + +
    +
    +
    +
    +
    +
    + + +
    +
    + + {!mini && ( +
    +
    +
    + {get_user("employee.name") + ? get_user("employee.name") + : "-"} +
    +
    + {get_user("email") ? get_user("email") : "-"} +
    +
    + + { + setProfile(open); + }} + open={profile} + content={ +
    +
    + Profile +
    +
    +
    { + 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" + > +
    +
    + + Switch Role +
    +
    +
    +
    { + 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" + > +
    +
    + + Sign Out +
    +
    +
    +
    +
    + } + > +
    + +
    +
    +
    + )} +
    +
    +
    +
    + ); +}; + +export default SidebarBetterTree; diff --git a/components/tablelist/TableBetter.tsx b/components/tablelist/TableBetter.tsx index bb308de..4b5cfca 100644 --- a/components/tablelist/TableBetter.tsx +++ b/components/tablelist/TableBetter.tsx @@ -1,33 +1,13 @@ "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 React, { useEffect, useState } from "react"; +import { Table } from "flowbite-react"; +import { HiChevronLeft, HiChevronRight } from "react-icons/hi"; 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 { toast } from "sonner"; 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 { formatMoney } from "../form/field/TypeInput"; -import { cloneFM } from "@/lib/utils/cloneFm"; -import { ResizableBox } from "react-resizable"; export const TableEditBetter: React.FC = ({ name, column, @@ -250,7 +230,14 @@ export const TableEditBetter: React.FC = ({ )} > {!disabledHeadTable ? ( - + {columns.map((col, idx) => { return ( @@ -265,7 +252,7 @@ export const TableEditBetter: React.FC = ({ }} >
    - {col?.name} + {col?.header()}
    ); @@ -277,9 +264,25 @@ export const TableEditBetter: React.FC = ({ )} {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 ( - + {columns.map((col, idx) => { const param = { row: row, @@ -300,7 +303,7 @@ export const TableEditBetter: React.FC = ({ key={`row_${name}_${index}_${col?.accessorKey}_${idx}`} className={"table-header-tbl capitalize"} > - {renderData} +
    {renderData}
    ); })} diff --git a/components/tablelist/TableList.tsx b/components/tablelist/TableList.tsx index bc49b6e..3b85c55 100644 --- a/components/tablelist/TableList.tsx +++ b/components/tablelist/TableList.tsx @@ -15,22 +15,21 @@ 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 { FaChevronUp } from "react-icons/fa6"; +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 { FaChevronDown } from "react-icons/fa"; 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 TableList: React.FC = ({ name, column, + style = "UI", align = "center", onLoad, take = 20, @@ -149,7 +148,18 @@ export const TableList: React.FC = ({ }); const cloneListFM = (data: any[]) => { 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(); } }; @@ -182,27 +192,38 @@ export const TableList: React.FC = ({ local.render(); } - - if (Array.isArray(onLoad)) { - local.data = onLoad; - cloneListFM(onLoad); + if (mode === "form") { + local.data = fm.data?.[name] || []; + cloneListFM(fm.data?.[name] || []); local.render(); - setData(onLoad); + setData(fm.data?.[name] || []); } 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 (Array.isArray(onLoad)) { + local.data = onLoad; + cloneListFM(onLoad); + local.render(); + setData(onLoad); + } else if (typeof onLoad === "function") { + const res: any = await onLoad({ + search: local.search, + sort: local.sort, + take, + paging: 1, + }); + local.data = res; + cloneListFM(res); + local.render(); + setData(res); + } else { + local.data = onLoad; + cloneListFM(onLoad); + local.render(); + setData(onLoad); + } } + setTimeout(() => { + toast.dismiss(); + }, 2000); }; if (typeof onInit === "function") { onInit(local); @@ -337,7 +358,7 @@ export const TableList: React.FC = ({ <>
    {!disabledHeader ? ( -
    +
    @@ -399,28 +420,28 @@ export const TableList: React.FC = ({
    = ({ )} > {!disabledHeadTable ? ( - + {table.getHeaderGroups().map((headerGroup) => ( = ({ {isSort ? (
    - - + {local?.sort?.[name] ? ( + <> + {local?.sort?.[name] === "asc" ? ( + + ) : ( + + )} + + ) : ( + + )}
    ) : ( <> )} - - {headerGroup.headers.length !== index + 1 ? ( + {resize && + headerGroup.headers.length !== index + 1 ? (
    = ({ onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), className: cx( - `resizer bg-[#b3c9fe] cursor-e-resize ${ + `resizer hover:bg-second cursor-e-resize ${ table.options.columnResizeDirection } ${ header.column.getIsResizing() - ? "isResizing" - : "" + ? "isResizing bg-second" + : " bg-primary" }`, css` - width: 1px; + width: 5px; cursor: e-resize !important; ` ), @@ -608,7 +645,7 @@ export const TableList: React.FC = ({ <> )} - + {table.getRowModel().rows.map((row, idx) => { const fm_row = mode === "form" ? local.dataForm?.[idx] : null; @@ -623,7 +660,8 @@ export const TableList: React.FC = ({ vertical-align: ${align}; } `, - "border-none" + "border-none ", + style === "UI" ? "even:bg-linear-blue " : "" )} > {row.getVisibleCells().map((cell: any) => { @@ -737,7 +775,7 @@ export const Pagination: React.FC = ({ local.render(); }, [page, count]); return ( -
    +
    Showing {local.page * 20 - 19} to{" "} {list.data?.length >= 20 @@ -843,7 +881,7 @@ export const PaginationPage: React.FC = ({ local.render(); }, [page, count]); return ( -
    +
    = ({ + name, + column, + align = "center", + onLoad, + take = 20, + header, + disabledPagination, + disabledHeader, + disabledHeadTable, + hiddenNoRow, + disabledHoverRow, + onInit, + onCount, + fm, + mode, + feature, + onChange, +}) => { + const [data, setData] = useState([]); + 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( + <> + + {"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( + <> + + {"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[] = init_column(column); + const [sorting, setSorting] = React.useState([]); + const [columns] = React.useState(() => + checkbox + ? [ + { + id: "select", + width: 10, + header: ({ table }) => ( + { + 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 ( +
    + { + 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(); + }} + /> +
    + ); + }, + sortable: false, + }, + ...defaultColumns, + ] + : [...defaultColumns] + ); + const [columnResizeMode, setColumnResizeMode] = + React.useState("onChange"); + + const [columnResizeDirection, setColumnResizeDirection] = + React.useState("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 ( + <> +
    + {!disabledHeader ? ( +
    +
    +
    +
    + {sideLeft ? ( + sideLeft(local) + ) : ( + <> + + + + + )} +
    +
    +
    + +
    +
    + { + e.preventDefault(); + await local.reload(); + }} + > + +
    + { + const value = e.target.value; + local.search = value; + local.render(); + handleSearch(); + }} + /> +
    + +
    +
    {sideRight ? sideRight(local) : <>}
    +
    +
    + ) : ( + <> + )} + +
    +
    +
    +
    +
    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 ? ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {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 ( + + ); + })} + + ))} + + ) : ( + <> + )} + + + {table.getRowModel().rows.map((row, idx) => { + const fm_row = + mode === "form" ? local.dataForm?.[idx] : null; + return ( + 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 ( + + {renderData} + + ); + })} + + ); + })} + +
    +
    { + 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" : "" + )} + > +
    + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
    + {isSort ? ( +
    + {local?.sort?.[name] ? ( + <> + {local?.sort?.[name] === "asc" ? ( + + ) : ( + + )} + + ) : ( + + )} +
    + ) : ( + <> + )} +
    + {resize && + headerGroup.headers.length !== index + 1 ? ( +
    + 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)` + : "", + }, + }} + >
    + ) : null} +
    +
    +
    + {!hiddenNoRow && !table.getRowModel().rows?.length && ( +
    +
    + +
    No Data
    +
    +
    + )} +
    +
    + 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); + }} + /> +
    + + ); +}; + +export const Pagination: React.FC = ({ + 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 ( +
    +
    + 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 +
    +
    +
    + +
    +
    +
    +
    +
    { + 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" + )} + > + + {/* Previous */} +
    +
    { + 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" + )} + > + {/* Next */} + +
    +
    +
    +
    + ); +}; +export const PaginationPage: React.FC = ({ + 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 ( +
    +
    +
    +
    { + 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 " + )} + > + +
    +
    +
    + +
    +
    +
    { + 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 " + )} + > + +
    +
    +
    +
    + ); +}; + +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; +}; diff --git a/components/tablelist/TableUI.tsx b/components/tablelist/TableUI.tsx new file mode 100644 index 0000000..6731a91 --- /dev/null +++ b/components/tablelist/TableUI.tsx @@ -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 = ({ + 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 ( +
    +
    + {title} +
    +
    +
    + {tab?.length && ( +
    +
    + { + console.log({ row }); + return ( +
    +
    + {getNumber(row?.count) > 999 + ? "99+" + : getNumber(row?.count)} +
    +
    +
    Total
    +
    {row.name}
    +
    +
    + ); + }} + 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 <>; + }} + /> +
    +
    + )} + +
    +
    + +
    +
    +
    +
    +
    + ); +}; diff --git a/components/tablist/TabHeaderBetter.tsx b/components/tablist/TabHeaderBetter.tsx new file mode 100644 index 0000000..145311e --- /dev/null +++ b/components/tablist/TabHeaderBetter.tsx @@ -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 = ({ + 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 ( +
    + + + {Array.isArray(onLoad) && + onLoad.map((e, idx) => { + return ( + { + 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)} +
    +
    + ); + })} +
    + {local.data.map((e) => { + return ( + +
    + {tabContent(e)} +
    +
    + ); + })} +
    +
    + ); +}; diff --git a/components/ui/Progress.tsx b/components/ui/Progress.tsx new file mode 100644 index 0000000..45a35fe --- /dev/null +++ b/components/ui/Progress.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/components/ui/scroll-area.tsx b/components/ui/scroll-area.tsx new file mode 100644 index 0000000..b82c04f --- /dev/null +++ b/components/ui/scroll-area.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + +
    {children}
    +
    + + +
    +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = "vertical", ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/components/ui/scroll-better.tsx b/components/ui/scroll-better.tsx new file mode 100644 index 0000000..f2642e1 --- /dev/null +++ b/components/ui/scroll-better.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +export default function CustomScroll({ children, className }: any) { + return ( +
    + {/* Kontainer Scroll */} +
    +
    + {/* Konten Scroll */} + {children} +
    +
    + + {/* Scrollbar Manual */} +
    +
    +
    +
    + ); +} diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx new file mode 100644 index 0000000..5a61012 --- /dev/null +++ b/components/ui/tooltip.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/package.json b/package.json index c999d7a..c2bfc30 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,11 @@ "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.3", "@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-tabs": "^1.1.1", + "@radix-ui/react-tooltip": "^1.1.7", "@react-pdf/renderer": "^4.1.5", "@tanstack/react-table": "^8.20.5", "@tiptap/extension-color": "^2.11.2",