first commit

This commit is contained in:
faisolavolut 2025-01-02 10:16:29 +07:00
commit a1612f527a
101 changed files with 14217 additions and 0 deletions

View File

@ -0,0 +1,4 @@
[data-floating-ui-portal] > div {
z-index: 100;
}

View File

@ -0,0 +1,376 @@
import {
FloatingFocusManager,
FloatingOverlay,
FloatingPortal,
Placement,
arrow,
autoUpdate,
flip,
offset,
shift,
useClick,
useDismiss,
useFloating,
useId,
useInteractions,
useMergeRefs,
useRole,
} from "@floating-ui/react";
import * as React from "react";
import "./Popover.css";
import { css } from "@emotion/css";
interface PopoverOptions {
initialOpen?: boolean;
placement?: Placement;
modal?: boolean;
open?: boolean;
offset?: number;
onOpenChange?: (open: boolean) => void;
autoFocus?: boolean;
backdrop?: boolean | "self";
root?: HTMLElement;
}
export function usePopover({
initialOpen = false,
placement = "bottom",
modal = false,
open: controlledOpen,
offset: popoverOffset = 5,
onOpenChange: setControlledOpen,
autoFocus = false,
backdrop = true,
root,
}: PopoverOptions = {}) {
const arrowRef = React.useRef<HTMLDivElement | null>(null);
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(initialOpen);
const [labelId, setLabelId] = React.useState<string | undefined>();
const [descriptionId, setDescriptionId] = React.useState<string | undefined>();
// Determine whether the popover is open
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = setControlledOpen ?? setUncontrolledOpen;
// Floating UI setup
const data = useFloating({
placement,
open,
onOpenChange: setOpen,
whileElementsMounted: autoUpdate,
middleware: [
offset(popoverOffset),
flip({
fallbackAxisSideDirection: "end",
padding: 8,
}),
shift({ padding: 5 }),
arrow({ element: arrowRef }),
],
});
const context = data.context;
// Add interactions (click, dismiss, role)
const click = useClick(context, {
enabled: controlledOpen == null, // Only enable if not controlled
});
const dismiss = useDismiss(context);
const role = useRole(context);
// Combine all interactions
const interactions = useInteractions([click, dismiss, role]);
// Return memoized popover properties
return React.useMemo(
() => ({
open,
setOpen,
...interactions,
...data,
arrowRef,
modal,
labelId,
descriptionId,
setLabelId,
setDescriptionId,
backdrop,
autoFocus,
root,
}),
[
open,
setOpen,
interactions,
data,
modal,
labelId,
descriptionId,
backdrop,
autoFocus,
root,
]
);
}
function mapPlacementSideToCSSProperty(placement: Placement) {
const staticPosition = placement.split("-")[0];
const staticSide = {
top: "bottom",
right: "left",
bottom: "top",
left: "right",
}[staticPosition];
return staticSide;
}
function PopoverArrow() {
const context = usePopoverContext();
const { x: arrowX, y: arrowY } = context.middlewareData.arrow || {
x: 0,
y: 0,
};
const staticSide = mapPlacementSideToCSSProperty(context.placement) as string;
return (
<div
ref={context.arrowRef}
style={{
left: arrowX != null ? `${arrowX}px` : "",
top: arrowY != null ? `${arrowY}px` : "",
[staticSide]: "-4px",
transform: "rotate(45deg)",
cursor: "pointer",
}}
className={cx(
"arrow",
css`
pointer-events: none;
position: absolute;
width: 10px;
height: 10px;
background: white;
`
)}
/>
);
}
type ContextType =
| (ReturnType<typeof usePopover> & {
setLabelId: React.Dispatch<React.SetStateAction<string | undefined>>;
setDescriptionId: React.Dispatch<
React.SetStateAction<string | undefined>
>;
})
| null;
const PopoverContext = React.createContext<ContextType>(null);
export const usePopoverContext = () => {
const context = React.useContext(PopoverContext);
if (context == null) {
throw new Error("Popover components must be wrapped in <Popover />");
}
return context;
};
export function Popover({
children,
content,
modal = false,
className,
classNameTrigger,
arrow,
...restOptions
}: {
root?: HTMLElement;
className?: string;
classNameTrigger?: string;
children: React.ReactNode;
content?: React.ReactNode;
arrow?: boolean;
} & PopoverOptions) {
const popover = usePopover({ modal, ...restOptions });
let _content = content;
if (!content) _content = <div className={"w-[300px] h-[150px]"}></div>;
return (
<PopoverContext.Provider value={popover}>
<PopoverTrigger
asChild
className={cx("h-full cursor-pointer", classNameTrigger)}
onClick={
typeof restOptions.open !== "undefined"
? () => {
popover.setOpen(!popover.open);
}
: undefined
}
>
{[children]}
</PopoverTrigger>
<PopoverContent
className={cx(
className,
css`
background: white;
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
user-select: none;
`,
)}
>
{_content}
{(typeof arrow === "undefined" || arrow) && <PopoverArrow />}
</PopoverContent>
</PopoverContext.Provider>
);
}
interface PopoverTriggerProps {
children: React.ReactNode;
asChild?: boolean;
}
export const PopoverTrigger = React.forwardRef<
HTMLElement,
React.HTMLProps<HTMLElement> & PopoverTriggerProps
>(function PopoverTrigger({ children, asChild = false, ...props }, propRef) {
const context = usePopoverContext();
// Gabungkan refs dari popover context dan ref prop
const ref = useMergeRefs([context.refs.setReference, propRef]);
// `asChild` memungkinkan elemen anak digunakan sebagai anchor
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
ref,
...context.getReferenceProps({
...props,
...children.props,
"data-state": context.open ? "open" : "closed",
}),
} as any);
}
return (
<div
ref={ref}
data-state={context.open ? "open" : "closed"}
{...context.getReferenceProps(props as any)}
>
{children}
</div>
);
});
export const PopoverContent = React.forwardRef<
HTMLDivElement,
React.HTMLProps<HTMLDivElement>
>(function PopoverContent(props, propRef) {
const { context: floatingContext, ...context } = usePopoverContext();
const ref = useMergeRefs([context.refs.setFloating, propRef]);
if (!floatingContext.open) return null;
const _content = (
<div
ref={ref}
style={{
...context.floatingStyles,
...props.style,
}}
aria-labelledby={context.labelId}
aria-describedby={context.descriptionId}
{...context.getFloatingProps(props as any)}
>
{props.children}
</div>
);
const content = context.autoFocus ? (
<FloatingFocusManager context={floatingContext} modal={context.modal}>
{_content}
</FloatingFocusManager>
) : (
_content
);
return (
<FloatingPortal root={context.root}>
{context.backdrop ? (
<FloatingOverlay className={"z-50"} lockScroll>
{content}
</FloatingOverlay>
) : (
content
)}
</FloatingPortal>
);
});
export const PopoverHeading = React.forwardRef<
HTMLHeadingElement,
React.HTMLProps<HTMLHeadingElement>
>(function PopoverHeading({ children, ...props }, ref) {
const { setLabelId } = usePopoverContext();
const id = useId();
// Only sets `aria-labelledby` on the Popover root element
// if this component is mounted inside it.
React.useLayoutEffect(() => {
setLabelId(id);
return () => setLabelId(undefined);
}, [id, setLabelId]);
return (
<h2 {...props} ref={ref} id={id}>
{children}
</h2>
);
});
export const PopoverDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLProps<HTMLParagraphElement>
>(function PopoverDescription({ children, ...props }, ref) {
const { setDescriptionId } = usePopoverContext();
const id = useId();
// Only sets `aria-describedby` on the Popover root element
// if this component is mounted inside it.
React.useLayoutEffect(() => {
setDescriptionId(id);
return () => setDescriptionId(undefined);
}, [id, setDescriptionId]);
return (
<p {...props} ref={ref} id={id}>
{children}
</p>
);
});
export const PopoverClose = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(function PopoverClose(props, ref) {
const { setOpen } = usePopoverContext();
return (
<button
type="button"
ref={ref}
{...props}
onClick={(event) => {
props.onClick?.(event);
setOpen(false);
}}
/>
);
});

16
components/card.tsx Normal file
View File

@ -0,0 +1,16 @@
'use client';
import React, { ReactNode } from 'react';
interface CardProps {
className?: string;
children: ReactNode;
}
export const Card: React.FC<CardProps> = ({ className, children }) => {
return (
<div className={`bg-white shadow-lg shadow-gray-200 rounded-2xl p-4 ${className ? className : ''}`}>
{children}
</div>
);
};

28
components/comp/500.tsx Normal file
View File

@ -0,0 +1,28 @@
import { Button } from "flowbite-react";
import type { FC } from "react";
import { HiChevronLeft } from "react-icons/hi";
import { ButtonLink } from "../ui/button-link";
import { siteurl } from "@/lib/utils/siteurl";
const ServerErrorPage: FC = function () {
return (
<div className="flex min-h-screen flex-col items-center justify-center py-16">
<img alt="" src={siteurl("/500.svg")} className="lg:max-w-md" />
<h1 className="mb-3 w-4/5 text-center text-2xl font-bold dark:text-white md:text-5xl">
403 Forbidden
</h1>
<p className="mb-6 w-4/5 text-center text-lg text-gray-500 dark:text-gray-300">
Oops! It seems you dont have the keys to this door. Why not grab a
coffee while you figure things out? If you think this is a mistake,
contact your administrator.
</p>
<ButtonLink href={process.env.NEXT_PUBLIC_API_PORTAL}>
<div className="mr-1 flex items-center gap-x-2">
<HiChevronLeft className="text-xl" /> Go portal
</div>
</ButtonLink>
</div>
);
};
export default ServerErrorPage;

View File

@ -0,0 +1,168 @@
import { FC } from "react";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/lib/components/ui/dialog";
import { ButtonBetter, ButtonContainer } from "@/lib/components/ui/button";
import { IoEye } from "react-icons/io5";
import { HiPlus } from "react-icons/hi";
import { formatMoney } from "../form/field/TypeInput";
import { get_user } from "@/lib/utils/get_user";
import api from "@/lib/utils/axios";
import { toast } from "sonner";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { getNumber } from "@/lib/utils/getNumber";
import { events } from "@/lib/utils/event";
import get from "lodash.get";
export const AlertBatch: FC<any> = ({ local }) => {
// const fm = useLocal({
// // open:
// })
return (
<>
<Dialog>
<DialogTrigger asChild>
<div className="flex flex-row flex-grow">
<ButtonContainer className="bg-primary">
<div className="flex items-center gap-x-0.5">
<HiPlus className="text-xl" />
<span className="capitalize">Create Batch</span>
</div>
</ButtonContainer>
</div>
</DialogTrigger>
<DialogContent className=" flex flex-col">
<DialogHeader>
<DialogTitle>Create Batch</DialogTitle>
<DialogDescription className="hidden"></DialogDescription>
</DialogHeader>
<div className="flex items-center flex-row space-x-2 flex-grow">
<div className={cx(" flex flex-col flex-grow")}>
Are you sure to continue this action?
{local?.location_null
? ` There are ${formatMoney(
local?.location_null
)} locations NULL`
: ``}
</div>
</div>
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<ButtonBetter variant={"outline"}>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">No</span>
</div>
</ButtonBetter>
</DialogClose>
<DialogClose
asChild
onClick={async () => {
try {
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);
}
}
`
)}
/>
{"Saving..."}
</>
);
if (local.batch_lines?.length) {
const data = {
approver_id: get_user("employee.id"),
approver_name: get_user("employee.name"),
batch_lines: local.batch_lines?.length
? local.batch_lines.map((e: any) => {
return {
mp_planning_header_id: e,
};
})
: [],
};
await api.post(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/create`,
data
);
local.can_add = false;
local.render();
try {
const batch: any = await api.get(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/find-by-status/NEED APPROVAL`
);
local.batch = batch?.data?.data;
} catch (ex) {}
try {
const batch_ceo: any = await api.get(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/find-by-status/APPROVED`
);
local.batch = batch_ceo?.data?.data;
} catch (ex) {}
local.render();
}
setTimeout(() => {
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
Record Saved
</div>
</div>
);
}, 1000);
} catch (ex: any) {
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
Create Batch Failed { get(ex, "response.data.meta.message") || ex.message}.
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
}}
>
<ButtonBetter>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">Yes</span>
</div>
</ButtonBetter>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -0,0 +1,133 @@
import { FC } from "react";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/lib/components/ui/dialog";
import { ButtonBetter, ButtonContainer } from "@/lib/components/ui/button";
import { IoEye } from "react-icons/io5";
import { HiPlus } from "react-icons/hi";
import api from "@/lib/utils/axios";
import { get_user } from "@/lib/utils/get_user";
import { toast } from "sonner";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import get from "lodash.get";
export const AlertCeoApprove: FC<any> = ({ fm }) => {
return (
<>
<Dialog>
<DialogTrigger asChild>
<div className="flex flex-row flex-grow">
<ButtonBetter>Approve</ButtonBetter>
</div>
</DialogTrigger>
<DialogContent className=" flex flex-col">
<DialogHeader>
<DialogTitle>Approve</DialogTitle>
<DialogDescription>
Are You Sure to Approve This Batch?
</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<ButtonBetter variant={"outline"}>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">No</span>
</div>
</ButtonBetter>
</DialogClose>
<DialogClose asChild>
<ButtonBetter
onClick={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);
}
}
`
)}
/>
{"Saving..."}
</>
);
try {
const batch = await api.get(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/find-by-status/NEED APPROVAL`
);
const id = batch?.data?.data?.id;
const param = {
id,
status: "APPROVED",
approved_by: get_user("employee.id"),
approver_name: get_user("employee.name"),
};
const res = await api.put(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/update-status`,
param
);
fm.data = null;
fm.render();
setTimeout(() => {
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
Record Saved
</div>
</div>
);
}, 1000);
} catch (ex: any) {
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
Submit Failed { get(ex, "response.data.meta.message") || ex.message}.
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
}}
>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">Yes</span>
</div>
</ButtonBetter>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -0,0 +1,139 @@
import { FC } from "react";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/lib/components/ui/dialog";
import { ButtonBetter, ButtonContainer } from "@/lib/components/ui/button";
import { IoEye } from "react-icons/io5";
import { HiPlus } from "react-icons/hi";
import api from "@/lib/utils/axios";
import { get_user } from "@/lib/utils/get_user";
import { toast } from "sonner";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { getParams } from "@/lib/utils/get-params";
import get from "lodash.get";
export const AlertCeoApproveMPR: FC<any> = ({fm}) => {
const id = getParams("id");
return (
<>
<Dialog>
<DialogTrigger asChild>
<div className="flex flex-row flex-grow">
<ButtonBetter>Approve</ButtonBetter>
</div>
</DialogTrigger>
<DialogContent className=" flex flex-col">
<DialogHeader>
<DialogTitle>Approve</DialogTitle>
<DialogDescription>
Are You Sure to Approve This?
</DialogDescription>
</DialogHeader>
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<ButtonBetter variant={"outline"}>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">No</span>
</div>
</ButtonBetter>
</DialogClose>
<DialogClose asChild>
<ButtonBetter
onClick={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);
}
}
`
)}
/>
{"Saving..."}
</>
);
try {
const param = {
id,
status: "APPROVED",
level: "Level CEO",
approver_id: get_user("employee.id"),
approved_by: get_user("employee.name"),
};
const formData = new FormData();
formData.append("payload", JSON.stringify(param));
const res = await api.put(
`${process.env.NEXT_PUBLIC_API_MPP}/api/mp-requests/status`,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);
setTimeout(() => {
fm.data.is_approve = false;
fm.render();
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
Record Saved
</div>
</div>
);
}, 1000);
} catch (ex: any) {
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
Submit Failed { get(ex, "response.data.meta.message") || ex.message}.
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
}}
>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">Yes</span>
</div>
</ButtonBetter>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -0,0 +1,317 @@
import { FC, useEffect } from "react";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/lib/components/ui/dialog";
import { ButtonBetter, ButtonContainer } from "@/lib/components/ui/button";
import { Checkbox } from "@/lib/components/ui/checkbox";
import { IoEye } from "react-icons/io5";
import { HiPlus } from "react-icons/hi";
import { useLocal } from "@/lib/utils/use-local";
import api from "@/lib/utils/axios";
import { Form } from "../form/Form";
import { Field } from "../form/Field";
import { cloneFM } from "@/lib/utils/cloneFm";
import { toast } from "sonner";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { get_user } from "@/lib/utils/get_user";
import { events } from "@/lib/utils/event";
import get from "lodash.get";
export const AlertCeoReject: FC<any> = ({ lc }) => {
const local = useLocal({
organization: [] as any[],
reject: "reject-all" as any,
fm: null as any,
org: [] as string[]
});
useEffect(() => {
const run = async () => {
const batch: any = await api.get(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/find-by-status/NEED APPROVAL`
);
const btc = batch?.data?.data;
const addtional = {
status: "APPROVED",
paging: 1,
take: 500,
};
const params = await events("onload-param", addtional);
const res: any = await api.get(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/organizations/${btc?.id}` +
params
);
const data: any[] = res.data.data;
const result = data?.length
? data.map((e) => {
return { id: e.id, label: e.name };
})
: [];
local.organization = result;
local.render();
};
run();
}, []);
const items = [
{
id: "reject-all",
label: "Reject All",
},
{
id: "reject-partially",
label: "Reject Partially",
},
] as const;
return (
<>
<Dialog>
<DialogTrigger asChild>
<div className="flex flex-row flex-grow">
<ButtonBetter variant={"reject"}>Reject</ButtonBetter>
</div>
</DialogTrigger>
<DialogContent className=" flex flex-col">
<DialogHeader>
<DialogTitle>Reject</DialogTitle>
<DialogDescription className="hidden"></DialogDescription>
</DialogHeader>
<div className="flex flex-col flex-grow">
<div className="flex flex-col gap-y-2 mb-2">
{items.map((item) => (
<div
className="flex items-center space-x-2"
key={"checkbox_item_reject" + item.id}
>
<Checkbox
id={item.id}
checked={local?.reject === item.id}
onCheckedChange={(e) => {
local.reject = item.id;
local.render();
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{item.label}
</label>
</div>
))}
</div>
{local?.reject === "reject-partially" &&
local?.organization?.length && (
<>
<Form
onSubmit={async (fm: any) => {}}
onLoad={async () => {
return {
organization: [],
};
}}
showResize={false}
header={(fm: any) => {
return <></>;
}}
onInit={(fm: any) => {
local.fm = fm;
local.render();
}}
children={(fm: any) => {
return (
<div className="flex flex-col gap-y-2 flex-grow pl-6 max-h-[250px] overflow-y-scroll">
{local.organization.map((item) => {
const is_check = fm.data?.organization?.length
? fm.data.organization.find(
(org: any) => org?.id === item.id
)
: false;
const data = fm.data?.organization?.length
? fm.data.organization.find(
(org: any) => org?.id === item.id
)
: {};
return (
<div
className="flex flex-col"
key={"checkbox_item_reject" + item.id}
>
<div className="flex items-center space-x-2">
<Checkbox
id={item.id}
onCheckedChange={(e) => {
if (e) {
if (
!Array.isArray(fm.data?.organization)
) {
fm.data["organization"] = [];
fm.render();
}
// Jika checkbox dicentang, tambahkan item ke array organization
fm.data.organization.push({
id: item.id,
});
} else {
// Jika checkbox tidak dicentang, hapus item dari array organization
fm.data["organization"] = fm.data
?.organization?.length
? fm.data.organization.filter(
(org: any) => org?.id !== item.id
)
: [];
}
fm.render();
local.org = fm.data?.organization?.length ? fm.data.organization.map((e: any) => {
return {
id: e.id
}
}) : [];
local.render();
}}
/>
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{item.label}
</label>
</div>
{is_check ? (
<div className="pt-1 pb-3">
<Field
fm={cloneFM(fm, data)}
hidden_label={true}
name={"notes"}
label={"Organization"}
type={"text"}
placeholder="Notes"
/>
</div>
) : (
<></>
)}
</div>
);
})}
</div>
);
}}
/>
</>
)}
</div>
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<ButtonBetter variant={"outline"}>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">No</span>
</div>
</ButtonBetter>
</DialogClose>
<DialogClose
asChild
onClick={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);
}
}
`
)}
/>
{"Saving..."}
</>
);
try {
const isPartial = local.reject === "reject-partially";
if (isPartial) {
const partial = local?.org || [];
const res = await api.put(
`${process.env.NEXT_PUBLIC_API_MPP}/api/mp-plannings/lines/reject-partial-pt`,
{ approver_id: get_user("employee.id"), payload: partial }
);
} else {
const batch = await api.get(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/find-by-status/NEED APPROVAL`
);
const id = batch?.data?.data?.id;
const param = {
id,
status: "REJECTED",
approved_by: get_user("employee.id"),
approver_name: get_user("employee.name"),
};
const res = await api.put(
`${process.env.NEXT_PUBLIC_API_MPP}/api/batch/update-status`,
param
);
}
lc.data = null;
lc.render()
setTimeout(() => {
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
Record Saved
</div>
</div>
);
}, 1000);
} catch (ex: any) {
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
Submit Failed { get(ex, "response.data.meta.message") || ex.message}.
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
}}
>
<ButtonBetter>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">Yes</span>
</div>
</ButtonBetter>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
};

View File

@ -0,0 +1,203 @@
import { FC, useEffect } from "react";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/lib/components/ui/dialog";
import { ButtonBetter, ButtonContainer } from "@/lib/components/ui/button";
import { Checkbox } from "@/lib/components/ui/checkbox";
import { IoEye } from "react-icons/io5";
import { HiPlus } from "react-icons/hi";
import { useLocal } from "@/lib/utils/use-local";
import api from "@/lib/utils/axios";
import { Form } from "../form/Form";
import { Field } from "../form/Field";
import { cloneFM } from "@/lib/utils/cloneFm";
import { toast } from "sonner";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { get_user } from "@/lib/utils/get_user";
import { getParams } from "@/lib/utils/get-params";
import { Button } from "flowbite-react";
import get from "lodash.get";
export const AlertCeoRejectMPR: FC<any> = ({ lc }) => {
const id = getParams("id");
const local = useLocal({
organization: [] as any[],
reject: "reject-all" as any,
});
useEffect(() => {}, []);
const items = [
{
id: "reject-all",
label: "Reject All",
},
{
id: "reject-partially",
label: "Reject Partially",
},
] as const;
return (
<>
<Dialog>
<DialogTrigger asChild>
<div className="flex flex-row flex-grow">
<ButtonBetter variant={"reject"}>Reject</ButtonBetter>
</div>
</DialogTrigger>
<DialogContent className=" flex flex-col">
<DialogHeader>
<DialogTitle>Reject</DialogTitle>
<DialogDescription>Are You Sure to Reject This?</DialogDescription>
</DialogHeader>
<div className="flex flex-col flex-grow">
<Form
onSubmit={async (fm: any) => {}}
onLoad={async () => {
return {
organization: [],
};
}}
showResize={false}
header={(fm: any) => {
return <></>;
}}
children={(fm: any) => {
return (
<div className={cx("flex flex-col flex-wrap")}>
<div className="grid gap-4 mb-4 md:gap-6 md:grid-cols-2 sm:mb-8">
<div className="col-span-2">
<Field
fm={fm}
name={"notes"}
label={"Notes"}
type={"textarea"}
/>
</div>
</div>
</div>
);
}}
onFooter={(fm: any) => {
return (
<DialogFooter className="sm:justify-end">
<DialogClose asChild>
<ButtonBetter variant={"outline"}>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">No</span>
</div>
</ButtonBetter>
</DialogClose>
{!fm.data?.notes ? (
<ButtonBetter onClick={() => {
fm.error["notes"] = "Field is required"
fm.render();
}}>Yes</ButtonBetter>
) : (
<DialogClose
asChild
onClick={async () => {
fm.error = {};
fm.render();
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);
}
}
`
)}
/>
{"Saving..."}
</>
);
try {
const param = {
id,
status: "REJECTED",
level: "Level CEO",
approver_id: get_user("employee.id"),
approved_by: get_user("employee.name"),
notes: fm.data?.notes
};
const formData = new FormData();
formData.append("payload", JSON.stringify(param));
const res = await api.put(
`${process.env.NEXT_PUBLIC_API_MPP}/api/mp-requests/status`,
formData,
{
headers: {
"Content-Type": "multipart/form-data",
},
}
);
setTimeout(() => {
lc.data.is_approve = false;
lc.render();
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
Record Saved
</div>
</div>
);
}, 1000);
} catch (ex: any) {
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
Submit Failed { get(ex, "response.data.meta.message") || ex.message}.
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
}}
>
<ButtonBetter>
<div className="flex items-center gap-x-0.5">
<span className="capitalize">Yes</span>
</div>
</ButtonBetter>
</DialogClose>
)}
</DialogFooter>
);
}}
/>
</div>
</DialogContent>
</Dialog>
</>
);
};

164
components/form/Field.tsx Normal file
View File

@ -0,0 +1,164 @@
import { useEffect } from "react";
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";
export const Field: React.FC<any> = ({
fm,
label,
name,
onLoad,
type,
placeholder,
required,
disabled,
hidden_label,
onChange,
className,
style,
}) => {
let result = null;
const is_disable = fm.mode === "view" ? true : disabled;
const error = fm.error?.[name];
useEffect(() => {
if (typeof fm.fields?.[name] !== "object") {
const fields = fm.fields?.[name];
fm.fields[name] = {
...fields,
label,
name,
onLoad,
type,
placeholder,
required,
disabled,
hidden_label,
onChange,
className,
style,
};
fm.render();
}
}, []);
return (
<>
<div
className={cx(
"flex",
style === "inline" ? "flex-row gap-x-1" : "flex-col"
)}
>
{!hidden_label ? (
<label
className={cx(
"block mb-2 text-md font-medium text-gray-900 text-sm",
style === "inline" ? "w-[100px]" : ""
)}
>
{label}
</label>
) : (
<></>
)}
<div
className={cx(
error
? "flex flex-row rounded-md flex-grow border-red-500 border"
: "flex flex-row rounded-md flex-grow",
is_disable ? "bg-gray-100" : ""
)}
>
{["upload"].includes(type) ? (
<>
<TypeUpload
fm={fm}
name={name}
on_change={onChange}
mode={"upload"}
/>
</>
) : ["multi-upload"].includes(type) ? (
<>
<TypeUpload
fm={fm}
name={name}
on_change={onChange}
mode={"upload"}
type="multi"
/>
</>
) : ["dropdown"].includes(type) ? (
<>
<TypeDropdown
fm={fm}
required={required}
name={name}
onLoad={onLoad}
placeholder={placeholder}
disabled={is_disable}
onChange={onChange}
/>
</>
) : ["multi-dropdown"].includes(type) ? (
<>
<TypeDropdown
fm={fm}
required={required}
name={name}
onLoad={onLoad}
placeholder={placeholder}
disabled={is_disable}
onChange={onChange}
mode="multi"
/>
</>
) : ["checkbox"].includes(type) ? (
<>
<FieldCheckbox
fm={fm}
name={name}
onLoad={onLoad}
placeholder={placeholder}
disabled={is_disable}
on_change={onChange}
className={className}
/>
</>
) : ["single-checkbox"].includes(type) ? (
<>
<FieldCheckbox
fm={fm}
name={name}
onLoad={onLoad}
placeholder={placeholder}
disabled={is_disable}
on_change={onChange}
className={className}
mode="single"
/>
</>
) : (
<>
<TypeInput
fm={fm}
name={name}
placeholder={placeholder}
required={required}
type={type}
disabled={is_disable}
onChange={onChange}
/>
</>
)}
</div>
{error ? (
<div className="text-sm text-red-500 py-1">{error}</div>
) : (
<></>
)}
</div>
</>
);
};

254
components/form/Form.tsx Normal file
View File

@ -0,0 +1,254 @@
"use client";
import { useLocal } from "@/lib/utils/use-local";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { ReactNode, useEffect, useState } from "react";
import { toast } from "sonner";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "../ui/resize";
import get from "lodash.get";
import { Skeleton } from "../ui/Skeleton";
type Local<T> = {
data: T | null;
submit: () => Promise<void>;
render: () => void;
};
export const Form: React.FC<any> = ({
children,
header,
onLoad,
onSubmit,
onFooter,
showResize,
mode,
className,
onInit,
}) => {
const local = useLocal({
ready: false,
data: null as any | null,
submit: 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);
}
}
`
)}
/>
{"Saving..."}
</>
);
try {
await onSubmit(local);
setTimeout(() => {
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
Record Saved
</div>
</div>
);
}, 1000);
} catch (ex: any) {
const msg = get(ex, "response.data.meta.message") || ex.message;
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
Submit Failed {msg}.
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
}
},
reload: async () => {
local.ready = false;
local.render();
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..."}
</>
);
local.data = null;
local.render();
const res = onLoad();
if (res instanceof Promise) {
res.then((data) => {
local.ready = true;
local.data = data;
local.render(); // Panggil render setelah data diperbarui
// toast.dismiss();
// toast.success("Data Loaded Successfully!");
});
} else {
local.ready = true;
local.data = res;
local.render(); // Panggil render untuk memicu re-render
toast.dismiss();
toast.success("Data Loaded Successfully!");
}
},
fields: {} as any,
render: () => {},
error: {} as any,
mode,
});
useEffect(() => {
if (typeof onInit === "function") {
onInit(local);
}
local.ready = false;
local.render();
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..."}
</>
);
const res = onLoad();
if (res instanceof Promise) {
res.then((data) => {
local.ready = true;
local.data = data;
local.render(); // Panggil render setelah data diperbarui
// toast.dismiss();
// toast.success("Data Loaded Successfully!");
});
} else {
local.ready = true;
local.data = res;
local.render(); // Panggil render untuk memicu re-render
toast.dismiss();
toast.success("Data Loaded Successfully!");
}
}, []);
// Tambahkan dependency ke header agar reaktif
const HeaderComponent = header(local);
if (!local.ready)
return (
<div className="flex flex-grow flex-row items-center justify-center">
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-16 w-[250px]" />
</div>
</div>
);
return (
<div className={`flex-grow flex-col flex ${className}`}>
<div className="flex flex-row">{header(local)}</div>
{showResize ? (
// Resize panels...
<ResizablePanelGroup direction="vertical" className="rounded-lg border">
<ResizablePanel className="border-none flex flex-col">
<form
className="flex flex-grow flex-col"
onSubmit={(e) => {
e.preventDefault();
local.submit();
}}
>
{local.ready ? (
children(local)
) : (
<div>
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
)}
</form>
</ResizablePanel>
<ResizableHandle className="border-none" />
<ResizablePanel className="border-t-2 flex flex-row flex-grow">
{typeof onFooter === "function" ? onFooter(local) : null}
</ResizablePanel>
</ResizablePanelGroup>
) : (
<>
<form
className="flex flex-grow flex-col flex-grow"
onSubmit={(e) => {
e.preventDefault();
local.submit();
}}
>
{local.ready ? (
children(local)
) : (
<div className="flex flex-grow flex-row items-center justify-center">
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-16 w-[200px]" />
</div>
</div>
)}
</form>
{typeof onFooter === "function" ? onFooter(local) : null}
</>
)}
</div>
);
};

View File

@ -0,0 +1,70 @@
"use client";
import { Form } from "./Form";
import { useEffect, useState } from "react";
export const FormBetter: React.FC<any> = ({
children,
header,
onTitle,
onLoad,
onSubmit,
onFooter,
showResize,
mode,
className,
onInit,
}) => {
const [fm, setFM] = useState<any>({
data: null as any,
});
useEffect(() => {}, [fm.data]);
return (
<div className="flex flex-col flex-grow">
{typeof fm === "object" && typeof onTitle === "function" ? (
<div className="flex flex-row w-full">{onTitle(fm)}</div>
) : (
<></>
)}
<div className="w-full flex-grow flex flex-row rounded-lg overflow-hidden">
<div className="w-full flex flex-row flex-grow bg-white rounded-lg relative overflow-y-scroll shadow">
<Form
{...{
children,
header,
onTitle,
onLoad,
onSubmit,
onFooter,
showResize,
mode,
className: cx(className, "absolute top-0 left-0 w-full"),
onInit: (form: any) => {
setFM(form);
const originalRender = form.render;
// 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);
}
},
}}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,325 @@
import { siteurl } from "@/lib/utils/siteurl";
import { useLocal } from "@/lib/utils/use-local";
import { ExternalLink } from "lucide-react";
import { ReactElement } from "react";
export const ThumbPreview = ({
url,
options,
}: {
url: string;
options: ReactElement;
}) => {
const local = useLocal({ size: "", is_doc: true }, async () => {
});
const file = getFileName(url);
if (typeof file === "string") return;
const color = darkenColor(generateRandomColor(file.extension));
let content = (
<div
className={cx(
css`
background: white;
color: ${color};
border: 1px solid ${color};
color: ${color};
border-radius: 3px;
text-transform: uppercase;
font-size: 14px;
font-weight: black;
padding: 3px 7px;
width: 60px;
height: 60px;
&:hover {
border: 1px solid #1c4ed8;
outline: 1px solid #1c4ed8;
}
`,
"flex justify-center items-center flex-col"
)}
onClick={() => {
// let _url = siteurl(url || "");
// window.open(_url, "_blank");
}}
>
<div>{file.extension}</div>
<div
className={css`
font-size: 9px;
color: gray;
margin-top: -3px;
`}
>
{local.size}
</div>
</div>
);
let is_image = false;
if ([".png", ".jpeg", ".jpg", ".webp"].find((e) => url.endsWith(e))) {
is_image = true;
local.is_doc = false;
content = (
<img
onClick={() => {
let _url = siteurl(url || "");
window.open(_url, "_blank");
}}
className={cx(
"rounded-md",
css`
&:hover {
outline: 2px solid #1c4ed8;
}
`,
css`
width: 60px;
height: 60px;
background-image: linear-gradient(
45deg,
#ccc 25%,
transparent 25%
),
linear-gradient(135deg, #ccc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(135deg, transparent 75%, #ccc 75%);
background-size: 25px 25px; /* Must be a square */
background-position: 0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px; /* Must be half of one side of the square */
`
)}
src={siteurl(
`/_img/${url.substring("_file/".length)}?${"w=60&h=60&fit=cover"}`
)}
/>
);
}
return (
<>
{file.extension && (
<div
className={cx(
"flex border rounded items-start px-1 relative bg-white cursor-pointer",
"space-x-1 py-1 thumb-preview"
)}
>
{content}
{options}
</div>
)}
</>
);
};
export const FilePreview = ({ url }: { url: string }) => {
const file = getFileName(url);
if (typeof file === "string")
return (
<div
className={cx(
css`
border-radius: 3px;
padding: 0px 5px;
height: 20px;
margin-right: 5px;
height: 20px;
border: 1px solid #ccc;
background: white;
`,
"flex items-center text-md"
)}
>
{file}
</div>
);
const color = darkenColor(generateRandomColor(file.extension));
let content = (
<div
className={cx(
css`
background: white;
border: 1px solid ${color};
color: ${color};
border-radius: 3px;
text-transform: uppercase;
padding: 0px 5px;
font-size: 9px;
height: 15px;
margin-right: 5px;
`,
"flex items-center"
)}
>
{file.extension}
</div>
);
if (url.startsWith("_file/")) {
if ([".png", ".jpeg", ".jpg", ".webp"].find((e) => url.endsWith(e))) {
content = (
<img
className={cx(
"my-1 rounded-md",
css`
background-image: linear-gradient(
45deg,
#ccc 25%,
transparent 25%
),
linear-gradient(135deg, #ccc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(135deg, transparent 75%, #ccc 75%);
background-size: 25px 25px; /* Must be a square */
background-position: 0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px; /* Must be half of one side of the square */
`
)}
src={siteurl(
`/_img/${url.substring("_file/".length)}?${"w=100&h=20"}`
)}
/>
);
}
}
return (
<>
{file.extension && (
<div
className={cx(
"flex border rounded items-center px-1 bg-white cursor-pointer",
"pr-2",
css`
&:hover {
border: 1px solid #1c4ed8;
outline: 1px solid #1c4ed8;
}
`
)}
onClick={() => {
let _url = siteurl(url || "");
window.open(_url, "_blank");
}}
>
{content}
<div className="ml-2">
<ExternalLink size="12px" />
</div>
</div>
)}
</>
);
};
function darkenColor(color: string, factor: number = 0.5): string {
const rgb = hexToRgb(color);
const r = Math.floor(rgb.r * factor);
const g = Math.floor(rgb.g * factor);
const b = Math.floor(rgb.b * factor);
return rgbToHex(r, g, b);
}
function hexToRgb(hex: string): { r: number; g: number; b: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
}
: { r: 0, g: 0, b: 0 };
}
function rgbToHex(r: number, g: number, b: number): string {
return `#${r.toString(16).padStart(2, "0")}${g
.toString(16)
.padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
}
function generateRandomColor(str: string): string {
let hash = 0;
if (str.length === 0) return hash.toString(); // Return a string representation of the hash
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255;
color += ("00" + value.toString(16)).substr(-2);
}
return color;
}
const getFileName = (url: string) => {
if (url.startsWith("[")) {
try {
const list = JSON.parse(url);
if (list.length === 0) return "Empty";
return `${list.length} File${list.length > 1 ? "s" : ""}`;
} catch (e) {
console.error(`Error parsing multi-file: ${url}`);
}
return "Unknown File";
}
const fileName = url.substring(url.lastIndexOf("/") + 1);
const dotIndex = fileName.lastIndexOf(".");
const fullname = fileName;
if (dotIndex === -1) {
return { name: fileName, extension: "", fullname };
}
const name = fileName.substring(0, dotIndex);
const extension = fileName.substring(dotIndex + 1);
return { name, extension, fullname };
};
export const ImgThumb = ({
className,
url,
w,
h,
fit,
}: {
className?: string;
url: string;
w: number;
h: number;
fit?: "cover" | "contain" | "inside" | "fill" | "outside";
}) => {
const local = useLocal({ error: false });
return (
<div
className={cx(
"img-thumb",
className,
css`
width: ${w}px;
height: ${h}px;
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(135deg, #ccc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(135deg, transparent 75%, #ccc 75%);
background-size: 25px 25px; /* Must be a square */
background-position: 0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px; /* Must be half of one side of the square */
`
)}
>
{!local.error && url && (
<img
onError={() => {
local.error = true;
local.render();
}}
src={siteurl(
`/_img/${url.substring("_file/".length)}?w=${w}&h=${h}&fit=${
fit || "cover"
}`
)}
/>
)}
</div>
);
};

View File

@ -0,0 +1,14 @@
import { TextInput } from "flowbite-react";
export const TypeInput: React.FC<any> = () => {
return (
<>
<TextInput
id="name"
name="name"
placeholder="Type product name"
required
/>
</>
);
};

View File

@ -0,0 +1,147 @@
import { useLocal } from "@/lib/utils/use-local";
import { FC, useEffect } from "react";
import { ButtonBetter } from "../../ui/button";
export const FieldCheckbox: FC<any> = ({
fm,
name,
onLoad,
onChange,
placeholder,
disabled,
className,
mode,
}) => {
const local = useLocal({
list: [] as any[],
reload: async () => {
fm.fields[name] = { ...fm.fields?.[name], ...local };
fm.render();
const callback = (res: any[]) => {
if (Array.isArray(res)) {
local.list = res;
} else {
local.list = [];
}
local.render();
};
const res = onLoad();
if (res instanceof Promise) res.then(callback);
else callback(res);
},
});
useEffect(() => {
fm.fields[name] = { ...fm.fields?.[name], ...local };
const callback = (res: any[]) => {
if (Array.isArray(res)) {
local.list = res;
} else {
local.list = [];
}
local.render();
};
const res = onLoad();
if (res instanceof Promise) res.then(callback);
else callback(res);
}, []);
let value =
mode === "single" && typeof fm.data?.[name] === "string"
? [fm.data?.[name]]
: fm.data?.[name];
let is_tree = false;
const applyChanges = (selected: any[]) => {
selected = selected.filter((e) => e);
const val = selected.map((e) => e.value);
if (mode === "single") {
selected = val?.[0];
fm.data[name] = selected;
} else {
fm.data[name] = val;
}
fm.render();
};
return (
<>
<div className={cx("flex items-center w-full flex-row")}>
<div
className={cx(
`flex flex-col p-0.5 flex-1`,
!is_tree && "space-y-1 ",
className
)}
>
{local.list.map((item, idx) => {
let isChecked = false;
try {
isChecked = value.some((e: any) => e === item.value);
} catch (ex) {}
return (
<div
key={idx + "_checkbox"}
onClick={() => {
if (!disabled) {
let selected = Array.isArray(value)
? value.map((row) => {
return local.list.find((e) => e.value === row);
})
: [];
if (isChecked) {
selected = selected.filter(
(e: any) => e.value !== item.value
);
} else {
if (mode === "single") {
selected = [item];
} else {
selected.push(item);
}
}
applyChanges(selected);
}
}}
className={cx(
"text-sm opt-item flex flex-row space-x-1 cursor-pointer items-center rounded-full p-0.5",
isChecked && "active"
)}
>
{isChecked ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="fill-sky-500"
>
<path
fill="currentColor"
d="m10.6 14.092l-2.496-2.496q-.14-.14-.344-.15q-.204-.01-.364.15t-.16.354q0 .194.16.354l2.639 2.638q.242.243.565.243q.323 0 .565-.243l5.477-5.477q.14-.14.15-.344q.01-.204-.15-.363q-.16-.16-.354-.16q-.194 0-.353.16L10.6 14.092ZM5.615 20q-.69 0-1.152-.462Q4 19.075 4 18.385V5.615q0-.69.463-1.152Q4.925 4 5.615 4h12.77q.69 0 1.152.463q.463.462.463 1.152v12.77q0 .69-.462 1.152q-.463.463-1.153.463H5.615Z"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M5.615 20q-.69 0-1.152-.462Q4 19.075 4 18.385V5.615q0-.69.463-1.152Q4.925 4 5.615 4h12.77q.69 0 1.152.463q.463.462.463 1.152v12.77q0 .69-.462 1.152q-.463.463-1.153.463H5.615Zm0-1h12.77q.23 0 .423-.192q.192-.193.192-.423V5.615q0-.23-.192-.423Q18.615 5 18.385 5H5.615q-.23 0-.423.192Q5 5.385 5 5.615v12.77q0 .23.192.423q.193.192.423.192Z"
/>
</svg>
)}
<div className="">{item.label}</div>
</div>
);
})}
</div>
</div>
</>
);
};

View File

@ -0,0 +1,57 @@
import { useLocal } from "@/lib/utils/use-local";
import { Typeahead } from "./Typeahead";
import { useEffect } from "react";
export const TypeDropdown: React.FC<any> = ({
required,
fm,
name,
onLoad,
onChange,
placeholder,
disabled,
mode,
}) => {
return (
<>
<Typeahead
value={Array.isArray(fm.data?.[name]) ? fm.data?.[name] : fm.data?.[name] ? [fm.data?.[name]]: []}
disabledSearch={false}
// popupClassName={}
required={required}
onSelect={({ search, item }) => {
if (item) {
if (mode === "multi") {
if (!Array.isArray(fm.data[name])) {
fm.data[name] = [];
fm.render();
}
fm.data[name].push(item.value);
fm.render();
} else {
fm.data[name] = item.value;
fm.render();
}
}
if (typeof onChange === "function" && item) {
onChange(item);
}
return item?.value || search;
}}
disabled={disabled}
allowNew={false}
autoPopupWidth={true}
focusOpen={true}
mode={mode ? mode : "single"}
placeholder={placeholder}
options={onLoad}
onInit={(e) => {
fm.fields[name] = {
...fm.fields[name],
...e
}
}}
/>
</>
);
};

View File

@ -0,0 +1,277 @@
import { useLocal } from "@/lib/utils/use-local";
import Datepicker from "../../ui/Datepicker";
import { Input } from "../../ui/input";
import { Textarea } from "../../ui/text-area";
import { useEffect } from "react";
export const TypeInput: React.FC<any> = ({
name,
fm,
placeholder,
disabled = false,
required,
type,
field,
onChange,
}) => {
let value: any = fm.data?.[name] || "";
const input = useLocal({
value: 0 as any,
ref: null as any,
});
useEffect(() => {
if (type === "money") {
input.value =
typeof fm.data?.[name] === "number" && fm.data?.[name] === 0
? "0"
: formatCurrency(value);
input.render();
}
}, [fm.data?.[name]]);
const error = fm.error?.[name];
switch (type) {
case "textarea":
return (
<>
<Textarea
id={name}
name={name}
disabled={disabled}
required={required}
className={cx(
"text-sm",
error
? css`
border-color: red !important;
`
: ``,
css`
background-color: ${disabled
? "rgb(243 244 246)"
: "transparant"}
? "";
`
)}
placeholder={placeholder || ""}
value={value}
onChange={(ev) => {
fm.data[name] = ev.currentTarget.value;
fm.render();
if (typeof onChange === "function") {
onChange(fm.data[name]);
}
}}
/>
</>
);
break;
case "date":
return (
<>
<Datepicker
value={{ startDate: value, endDate: value }}
disabled={disabled}
displayFormat="DD MMM YYYY"
mode={"daily"}
maxDate={field?.max_date instanceof Date ? field.max_date : null}
minDate={field?.min_date instanceof Date ? field.min_date : null}
asSingle={true}
useRange={false}
onChange={(value) => {
fm.data[name] = value?.startDate
? new Date(value?.startDate)
: null;
fm.render();
if (typeof onChange === "function") {
onChange(fm.data[name]);
}
}}
/>
</>
);
break;
case "money":
return (
<>
<Input
id={name}
name={name}
disabled={disabled}
className={cx(
"text-sm text-right ",
error
? css`
border-color: red !important;
`
: ``,
css`
background-color: ${disabled
? "rgb(243 244 246)"
: "transparant"}
? "";
`
)}
required={required}
placeholder={placeholder || ""}
value={formatCurrency(input.value)}
type={"text"}
onChange={(ev) => {
const rawValue = ev.currentTarget.value
.replace(/[^0-9,-]/g, "")
.toString();
if (rawValue === "0") {
input.value = "0";
input.render();
}
if (
(!rawValue.startsWith(",") || !rawValue.endsWith(",")) &&
!rawValue.endsWith("-") &&
convertionCurrencyNumber(rawValue) !==
convertionCurrencyNumber(input.value)
) {
fm.data[name] = convertionCurrencyNumber(
formatCurrency(rawValue)
);
fm.render();
if (typeof onChange === "function") {
onChange(fm.data[name]);
}
input.value = formatCurrency(fm.data[name]);
input.render();
} else {
input.value = rawValue;
input.render();
}
}}
/>
</>
);
break;
}
return (
<>
<Input
id={name}
name={name}
className={cx(
"text-sm",
error
? css`
border-color: red !important;
`
: ``,
css`
background-color: ${disabled ? "rgb(243 244 246)" : "transparant"} ?
"";
`
)}
disabled={disabled}
required={required}
placeholder={placeholder || ""}
value={value}
type={!type ? "text" : type}
onChange={(ev) => {
fm.data[name] = ev.currentTarget.value;
fm.render();
if (typeof onChange === "function") {
onChange(fm.data[name]);
}
}}
/>
</>
);
};
const convertionCurrencyNumber = (value: string) => {
if (!value) return null;
let numberString = value.toString().replace(/[^0-9,-]/g, "");
if (numberString.endsWith(",")) {
return Number(numberString.replace(",", "")) || 0;
}
if (numberString.endsWith("-")) {
return Number(numberString.replace("-", "")) || 0;
}
const rawValue = numberString.replace(/[^0-9,-]/g, "").replace(",", ".");
return parseFloat(rawValue) || 0;
};
const formatCurrency = (value: any) => {
// Menghapus semua karakter kecuali angka, koma, dan tanda minusif (value === null || value === undefined) return '';
if (typeof value === "number" && value === 0) return "0";
if (typeof value === "string" && value === "0") return "0";
if (!value) return "";
let numberString = "";
if (typeof value === "number") {
numberString = formatMoney(value);
} else {
numberString = value.toString().replace(/[^0-9,-]/g, "");
}
if (numberString.endsWith("-") && numberString.startsWith("-")) {
return "-";
} else if (numberString.endsWith(",")) {
const isNegative = numberString.startsWith("-");
numberString = numberString.replace("-", "");
const split = numberString.split(",");
if (isNumberOrCurrency(split[0]) === "Number") {
split[0] = formatMoney(Number(split[0]));
}
let rupiah = split[0];
rupiah = split[1] !== undefined ? rupiah + "," + split[1] : rupiah;
return (isNegative ? "-" : "") + rupiah;
} else {
const isNegative = numberString.startsWith("-");
numberString = numberString.replace("-", "");
const split = numberString.split(",");
if (isNumberOrCurrency(split[0]) === "Number") {
split[0] = formatMoney(Number(split[0]));
}
let rupiah = split[0];
rupiah = split[1] !== undefined ? rupiah + "," + split[1] : rupiah;
return (isNegative ? "-" : "") + rupiah;
}
};
export const formatMoney = (res: any) => {
if (typeof res === "string" && res.startsWith("BigInt::")) {
res = res.substring(`BigInt::`.length);
}
const formattedAmount = new Intl.NumberFormat("id-ID", {
minimumFractionDigits: 0,
}).format(res);
return formattedAmount;
};
const isNumberOrCurrency = (input: any) => {
// Pengecekan apakah input adalah angka biasa
if (typeof input === "string") {
let rs = input;
if (input.startsWith("-")) {
rs = rs.replace("-", "");
}
const dots = rs.match(/\./g);
if (dots && dots.length > 1) {
return "Currency";
} else if (dots && dots.length === 1) {
if (!hasNonZeroDigitAfterDecimal(rs)) {
return "Currency";
}
return "Currency";
}
}
if (!isNaN(input)) {
return "Number";
}
// Pengecekan apakah input adalah format mata uang dengan pemisah ribuan
const currencyRegex = /^-?Rp?\s?\d{1,3}(\.\d{3})*$/;
if (currencyRegex.test(input)) {
return "Currency";
}
// Jika tidak terdeteksi sebagai angka atau format mata uang, kembalikan null atau sesuai kebutuhan
return null;
};
const hasNonZeroDigitAfterDecimal = (input: string) => {
// Ekspresi reguler untuk mencocokkan angka 1-9 setelah koma atau titik
const regex = /[.,]\d*[1-9]\d*/;
return regex.test(input);
};

View File

@ -0,0 +1,32 @@
import { FieldUploadMulti } from "./TypeUploadMulti";
import { FieldUploadSingle } from "./TypeUploadSingle";
export const TypeUpload: React.FC<any> = ({name, fm, on_change, mode, type}) => {
if(type === "multi"){
return (
<>
<FieldUploadMulti
field={{
name
}}
fm={fm}
on_change={on_change}
mode={mode}
/>
</>
);
}
return (
<>
<FieldUploadSingle
field={{
name
}}
fm={fm}
on_change={on_change}
mode={mode}
/>
</>
);
};

View File

@ -0,0 +1,177 @@
import get from "lodash.get";
import { Loader2, Paperclip, Trash2, Upload } from "lucide-react";
import { ChangeEvent, FC } from "react";
import * as XLSX from "xlsx";
import { useLocal } from "@/lib/utils/use-local";
import { siteurl } from "@/lib/utils/siteurl";
import { FilePreview } from "./FilePreview";
import { Spinner } from "../../ui/spinner";
export const FieldUploadMulti: FC<{
field: any;
fm: any;
on_change: (e: any) => void | Promise<void>;
mode?: "upload";
}> = ({ field, fm, on_change, mode }) => {
const styling = "mini";
const disabled = field?.disabled || false;
let value: any = fm.data?.[field.name];
// let type_upload =
const input = useLocal({
value: 0 as any,
display: false as any,
ref: null as any,
drop: false as boolean,
uploading: new Set<File>(),
fase: value ? "preview" : ("start" as "start" | "upload" | "preview"),
style: "inline" as "inline" | "full",
});
const on_upload = async (event: ChangeEvent<HTMLInputElement>) => {
let file = null;
try {
file = event.target?.files?.[0];
} catch (ex) {}
const upload_single = async (file: File) => {
return { url: `/dog.jpg` };
const formData = new FormData();
formData.append("file", file);
const url = "/api/upload";
const response = await fetch(url, {
method: "POST",
body: formData,
});
if (response.ok) {
const contentType: any = response.headers.get("content-type");
let result;
if (contentType.includes("application/json")) {
result = await response.json();
} else if (contentType.includes("text/plain")) {
result = await response.text();
} else {
result = await response.blob();
}
if (Array.isArray(result)) {
return `_file${get(result, "[0]")}`;
}
}
throw new Error("Upload Failed");
};
if (event.target.files) {
const list = [] as any[];
input.fase = "upload";
input.render();
const files = event.target.files.length;
for (let i = 0; i < event.target.files.length; i++) {
const file = event.target?.files?.item(i);
if (file) {
list.push({
name: file.name,
data: file,
});
}
}
fm.data[field.name] = list;
fm.render();
on_change(fm.data?.[field.name]);
input.fase = "start";
input.render();
}
if (input.ref) {
input.ref.value = null;
}
};
return (
<div className="flex-grow flex-col flex w-full h-full items-stretch p-1">
<div
className={cx(
"flex flex-row flex-wrap",
css`
flex-flow: row wrap;
`
)}
>
{input.fase === "upload" && (
<div
className={cx(
"flex gap-x-2 p-2 flex-row items-center border rounded-md border-gray-500",
css`
height: 30px;
`
)}
>
<Spinner /> <div>Uploading</div>
</div>
)}
</div>
<div className="flex pt-1">
<div
className={cx(
"button flex border rounded cursor-pointer hover:bg-blue-50",
css`
&:hover {
border: 1px solid #1c4ed8;
outline: 1px solid #1c4ed8;
}
`
)}
>
<div
className={cx(
"flex flex-row relative flex-grow pr-2 items-center ",
css`
padding-top: 3px;
padding-bottom: 2px;
input[type="file"],
input[type="file"]::-webkit-file-upload-button {
cursor: pointer;
}
`
)}
>
<input
ref={(ref) => {
if (!input.ref) {
input.ref = ref;
}
}}
type="file"
multiple={true}
accept={""}
onChange={on_upload}
className={cx(
"absolute w-full h-full cursor-pointer top-0 left-0 opacity-0"
)}
/>
{input.fase === "start" && (
<div
className={cx(
"items-center flex text-base px-1 outline-none rounded cursor-pointer"
)}
>
<div className="flex flex-row items-center px-2">
<Upload className="h-4 w-4" />
</div>
<div className="flex flex-row items-center text-sm">
Upload File
</div>
</div>
)}
</div>
</div>
<div
className="flex-1"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
></div>
</div>
</div>
);
};

View File

@ -0,0 +1,237 @@
import get from "lodash.get";
import { Loader2, Paperclip, Trash2, Upload } from "lucide-react";
import { ChangeEvent, FC } from "react";
import * as XLSX from "xlsx";
import { useLocal } from "@/lib/utils/use-local";
import { siteurl } from "@/lib/utils/siteurl";
import { FilePreview } from "./FilePreview";
export const FieldUploadSingle: FC<{
field: any;
fm: any;
on_change: (e: any) => void | Promise<void>;
mode?: "upload" | "import";
}> = ({ field, fm, on_change, mode }) => {
const styling = "mini";
const disabled = field?.disabled || false;
let value: any = fm.data?.[field.name];
// let type_upload =
const input = useLocal({
value: 0 as any,
display: false as any,
ref: null as any,
drop: false as boolean,
fase: value ? "preview" : ("start" as "start" | "upload" | "preview"),
style: "inline" as "inline" | "full",
});
const on_upload = async (event: ChangeEvent<HTMLInputElement>) => {
let file = null;
try {
file = event.target?.files?.[0];
} catch (ex) {}
if (mode === "import") {
const reader = new FileReader();
function arrayBufferToBinaryString(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer);
return String.fromCharCode.apply(null, Array.from(bytes));
}
reader.onload = (e: ProgressEvent<FileReader>) => {
if (e.target && e.target.result) {
const binaryStr =
typeof e.target.result === "string"
? e.target.result
: arrayBufferToBinaryString(e.target.result);
const workbook = XLSX.read(binaryStr, { type: "binary" });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
const jsonData = XLSX.utils.sheet_to_json(worksheet);
if (typeof on_change === "function") {
on_change({
value: jsonData,
file: file,
binnary: e.target.result,
});
}
}
};
if (file) {
if (typeof reader.readAsArrayBuffer === "function") {
reader.readAsArrayBuffer(file);
} else {
reader.readAsBinaryString(file);
}
}
} else if (file) {
const formData = new FormData();
formData.append("file", file);
let url = siteurl("/_upload");
if (
location.hostname === "prasi.avolut.com" ||
location.host === "localhost:4550"
) {
const newurl = new URL(location.href);
newurl.pathname = `/_proxy/${url}`;
url = newurl.toString();
}
input.fase = "upload";
input.render();
try {
const response = await fetch(url, {
method: "POST",
body: formData,
});
if (response.ok) {
const contentType: any = response.headers.get("content-type");
let result;
if (contentType.includes("application/json")) {
result = await response.json();
} else if (contentType.includes("text/plain")) {
result = await response.text();
} else {
result = await response.blob();
}
if (Array.isArray(result)) {
fm.data[field.name] = `_file${get(result, "[0]")}`;
fm.render();
setTimeout(() => {
input.fase = "preview";
input.render();
}, 1000);
} else {
input.fase = "start";
input.render();
alert("Error upload");
}
} else {
}
} catch (ex) {
input.fase = "start";
input.render();
alert("Error upload");
}
}
if (input.ref) {
input.ref.value = null;
}
};
return (
<div className="flex-grow flex-row flex w-full h-full items-stretch relative">
{input.fase === "start" ? (
<>
<div
className={cx(
"bg-gray-50 border border-gray-300 text-gray-900 text-md rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500",
css`
input[type="file"],
input[type="file"]::-webkit-file-upload-button {
cursor: pointer;
}
`
)}
>
{!disabled && (
<input
ref={(ref) => {
if(ref) input.ref = ref
}}
type="file"
multiple={false}
// accept={field.prop.upload?.accept}
accept={"file/**"}
onChange={on_upload}
className={cx(
"absolute w-full h-full cursor-pointer top-0 left-0 opacity-0"
)}
/>
)}
<div
onClick={() => {
if (input.ref) {
input.ref.click();
}
}}
className="items-center flex text-base px-1 outline-none rounded cursor-pointer "
>
<div className="flex flex-row items-center px-2">
<Upload className="h-4 w-4" />
</div>
<div className="flex flex-row items-center text-md">
Upload File
</div>
</div>
</div>
</>
) : input.fase === "upload" ? (
<div className="flex items-center">
<div className="px-2">
<Loader2 className={cx("h-5 w-5 animate-spin")} />
</div>
<div className="">Uploading</div>
</div>
) : input.fase === "preview" ? (
<div className="flex justify-between flex-1 p-1">
<FilePreview url={value || ""} />
{!disabled ? (
<>
<div
onClick={(e) => {
if (!disabled) {
e.preventDefault();
e.stopPropagation();
if (confirm("Clear this file ?")) {
input.fase = "start";
fm.data[field.name] = null;
fm.render();
}
}
}}
className={cx(
"flex flex-row items-center border px-2 rounded cursor-pointer hover:bg-red-100"
)}
>
<Trash2 className="text-red-500 h-4 w-4" />
</div>
</>
) : (
<></>
)}
</div>
) : (
<></>
)}
</div>
);
};
const IconFile: FC<{ type: string }> = ({ type }) => {
if (["xlsx"].includes(type)) {
return (
<div className="flex flex-row text-[#2a801d]">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="m2.859 2.877l12.57-1.795a.5.5 0 0 1 .571.494v20.848a.5.5 0 0 1-.57.494L2.858 21.123a1 1 0 0 1-.859-.99V3.867a1 1 0 0 1 .859-.99M17 3h4a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1h-4zm-6.8 9L13 8h-2.4L9 10.286L7.4 8H5l2.8 4L5 16h2.4L9 13.714L10.6 16H13z"
/>
</svg>
</div>
);
} else {
return (
<div className="flex flex-row ">
<Paperclip className="h-5 w-5" />
</div>
);
}
};

View File

@ -0,0 +1,631 @@
import { FC, KeyboardEvent, useCallback, useEffect, useRef } from "react";
import { useLocal } from "@/lib/utils/use-local";
import { TypeaheadOptions } from "./typeahead-opt";
import { Badge } from "../../ui/badge";
import { GoChevronDown } from "react-icons/go";
import { IoCloseOutline } from "react-icons/io5";
import { X } from "lucide-react";
import uniqBy from "lodash.uniqby";
type OptItem = { value: string; label: string; tag?: string };
export const Typeahead: FC<{
value?: string[] | null;
placeholder?: string;
required?: boolean;
options?: (arg: {
search: string;
existing: OptItem[];
}) => (string | OptItem)[] | Promise<(string | OptItem)[]>;
onSelect?: (arg: { search: string; item?: null | OptItem }) => string | false;
onChange?: (selected: string[]) => void;
unique?: boolean;
allowNew?: boolean;
className?: string;
popupClassName?: string;
localSearch?: boolean;
autoPopupWidth?: boolean;
focusOpen?: boolean;
disabled?: boolean;
mode?: "multi" | "single";
note?: string;
disabledSearch?: boolean;
onInit?: (e: any) => void;
}> = ({
value,
note,
options: options_fn,
onSelect,
unique,
allowNew: allow_new,
focusOpen: on_focus_open,
localSearch: local_search,
autoPopupWidth: auto_popup_width,
placeholder,
mode,
disabled,
onChange,
className,
popupClassName,
disabledSearch,
onInit,
}) => {
const local = useLocal({
value: [] as string[],
open: false,
options: [] as OptItem[],
loaded: false,
loading: false,
search: {
input: "",
timeout: null as any,
searching: false,
promise: null as any,
result: null as null | OptItem[],
},
unique: typeof unique === "undefined" ? true : unique,
allow_new: typeof allow_new === "undefined" ? false : allow_new,
on_focus_open: typeof on_focus_open === "undefined" ? true : on_focus_open,
local_search: typeof local_search === "undefined" ? true : local_search,
mode: typeof mode === "undefined" ? "multi" : mode,
auto_popup_width:
typeof auto_popup_width === "undefined" ? false : auto_popup_width,
select: null as null | OptItem,
});
const input = useRef<HTMLInputElement>(null);
let select_found = false;
let options = [...(local.search.result || local.options)];
const added = new Set<string>();
if (local.mode === "multi") {
options = options.filter((e) => {
if (!added.has(e.value)) added.add(e.value);
else return false;
if (local.select && local.select.value === e.value) select_found = true;
if (local.unique) {
if (local.value.includes(e.value)) {
return false;
}
}
return true;
});
if (!select_found) {
local.select = options[0];
}
}
useEffect(() => {
if (!value) return;
if (options.length === 0) {
loadOptions().then(() => {
if (typeof value === "object" && value) {
local.value = value;
local.render();
} else if (typeof value === "string") {
local.value = [value];
local.render();
}
});
} else {
if (typeof value === "object" && value) {
local.value = value;
local.render();
} else {
local.value = [];
local.render();
}
}
}, [value]);
const select = useCallback(
(arg: { search: string; item?: null | OptItem }) => {
if (!local.allow_new) {
let found = null;
if (!arg.item) {
found = options.find((e) => e.value === arg.search);
} else {
found = options.find((e) => e.value === arg.item?.value);
}
if (!found) {
return false;
}
}
if (local.unique) {
let found = local.value.find((e) => {
return e === arg.item?.value || arg.search === e;
});
if (found) {
return false;
}
}
if (local.mode === "single") {
local.value = [];
}
if (typeof onSelect === "function") {
const result = onSelect(arg);
if (result) {
local.value.push(result);
local.render();
if (typeof onChange === "function") {
onChange(local.value);
}
return result;
} else {
return false;
}
} else {
let val = false as any;
if (arg.item) {
local.value.push(arg.item.value);
val = arg.item.value;
} else {
if (!arg.search) return false;
local.value.push(arg.search);
val = arg.search;
}
if (typeof onChange === "function") {
onChange(local.value);
}
local.render();
return val;
}
return true;
},
[onSelect, local.value, options]
);
const keydown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Backspace") {
if (local.value.length > 0 && e.currentTarget.selectionStart === 0) {
local.value.pop();
local.render();
}
}
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
const selected = select({
search: local.search.input,
item: local.select,
});
if (local.mode === "single") {
local.open = false;
}
if (typeof selected === "string") {
if (!allow_new) resetSearch();
if (local.mode === "single") {
const item = options.find((item) => item.value === selected);
if (item) {
local.search.input = item.label;
}
}
}
local.render();
return;
}
if (options.length > 0) {
local.open = true;
if (e.key === "ArrowDown") {
e.preventDefault();
e.stopPropagation();
const idx = options.findIndex((item) => {
if (item.value === local.select?.value) return true;
});
if (idx >= 0) {
if (idx + 1 <= options.length - 1) {
local.select = options[idx + 1];
} else {
local.select = options[0];
}
} else {
local.select = options[0];
}
local.render();
}
if (e.key === "ArrowUp") {
e.preventDefault();
e.stopPropagation();
const idx = options.findIndex((item) => {
if (item.value === local.select?.value) return true;
});
if (idx >= 0) {
if (idx - 1 >= 0) {
local.select = options[idx - 1];
} else {
local.select = options[options.length - 1];
}
} else {
local.select = options[0];
}
local.render();
}
}
},
[local.value, local.select, select, options, local.search.input]
);
const loadOptions = useCallback(async () => {
if (typeof options_fn === "function" && !local.loading) {
local.loading = true;
local.loaded = false;
local.render();
const res = options_fn({
search: local.search.input,
existing: options,
});
if (res) {
const applyOptions = (result: (string | OptItem)[]) => {
local.options = result.map((item) => {
if (typeof item === "string") return { value: item, label: item };
return item;
});
local.render();
};
if (res instanceof Promise) {
const result = await res;
applyOptions(result);
} else {
applyOptions(res);
}
local.loaded = true;
local.loading = false;
local.render();
}
}
}, [options_fn]);
useEffect(() => {
if (typeof onInit === "function") {
onInit({
reload: async () => {
if (typeof options_fn === "function" && !local.loading) {
local.loading = true;
local.loaded = false;
local.render();
const res = options_fn({
search: local.search.input,
existing: options,
});
if (res) {
const applyOptions = (result: (string | OptItem)[]) => {
local.options = result.map((item) => {
if (typeof item === "string")
return { value: item, label: item };
return item;
});
local.render();
};
if (res instanceof Promise) {
const result = await res;
applyOptions(result);
} else {
applyOptions(res);
}
local.loaded = true;
local.loading = false;
local.render();
}
}
},
});
}
}, []);
const resetSearch = () => {
local.search.searching = false;
local.search.input = "";
local.search.promise = null;
local.search.result = null;
local.select = null;
clearTimeout(local.search.timeout);
};
if (local.mode === "single" && local.value.length > 1) {
local.value = [local.value.pop() || ""];
}
if (local.value.length === 0) {
if (local.mode === "single") {
if (!local.open && !allow_new) {
local.select = null;
local.search.input = "";
}
}
}
const valueLabel = uniqBy(
local.value?.map((value) => {
if (local.mode === "single") {
const item = options.find((item) => item.value === value);
if (!local.open && !allow_new) {
local.select = item || null;
local.search.input = item?.tag || item?.label || "";
}
return item;
}
const item = local.options.find((e) => e.value === value);
return item;
}),
"value"
);
let inputval = local.search.input;
if (!local.open && local.mode === "single" && local.value?.length > 0) {
const found = options.find((e) => e.value === local.value[0]);
if (found) {
inputval = found.tag || found.label;
} else {
inputval = local.value[0];
}
}
return (
<div className="flex flex-row flex-grow w-full relative">
<div
className={cx(
local.mode === "single" ? "cursor-pointer" : "cursor-text",
"text-black flex relative flex-wrap py-0 items-center w-full h-full flex-1 rounded-md border border-gray-300 overflow-hidden ",
className
)}
onClick={() => {
if (!disabled) input.current?.focus();
}}
>
{local.mode === "multi" ? (
<div
className={cx(
css`
margin-top: 5px;
margin-bottom: -3px;
`
)}
>
{valueLabel.map((e, idx) => {
return (
<Badge
key={idx}
variant={"outline"}
className={cx(
"space-x-1 mr-2 mb-2 bg-white",
!disabled &&
" cursor-pointer hover:bg-red-100 hover:border-red-100"
)}
onClick={(ev) => {
if (!disabled) {
ev.stopPropagation();
ev.preventDefault();
local.value = local.value.filter(
(val) => e?.value !== val
);
local.render();
input.current?.focus();
if (typeof onChange === "function") {
onChange(local.value);
}
}
}}
>
<div className="text-xs">
{e?.tag || e?.label || <>&nbsp;</>}
</div>
{!disabled && <IoCloseOutline size={12} />}
</Badge>
);
})}
</div>
) : (
<></>
)}
<TypeaheadOptions
popup={true}
onOpenChange={(open) => {
if (!open) {
local.select = null;
}
local.open = open;
local.render();
}}
showEmpty={!allow_new}
className={popupClassName}
open={local.open}
options={options}
searching={local.search.searching}
searchText={local.search.input}
onSelect={(value) => {
local.open = false;
resetSearch();
const item = options.find((item) => item.value === value);
if (item) {
let search = local.search.input;
if (local.mode === "single") {
local.search.input = item.tag || item.label;
} else {
local.search.input = "";
}
select({
search,
item,
});
}
local.render();
}}
width={
local.auto_popup_width ? input.current?.offsetWidth : undefined
}
isMulti={local.mode === "multi"}
selected={({ item, options, idx }) => {
if (item.value === local.select?.value) {
return true;
}
return false;
}}
>
<div
className="single flex-1 flex-grow flex flex-row cursor-pointer"
onClick={(e) => {
e.stopPropagation();
if (!disabled) {
if (!local.open) {
if (local.on_focus_open) {
loadOptions();
local.open = true;
local.render();
}
}
if (local.mode === "single") {
if (input && input.current) input.current.select();
}
}
}}
>
<input
placeholder={
local.mode === "multi" ? placeholder : valueLabel[0]?.label
}
type="text"
ref={input}
value={inputval}
onChange={async (e) => {
const val = e.currentTarget.value;
if (!local.open) {
local.open = true;
}
local.search.input = val;
local.render();
if (local.search.promise) {
await local.search.promise;
}
local.search.searching = true;
local.render();
if (local.search.searching) {
if (local.local_search) {
if (!local.loaded) {
await loadOptions();
}
const search = local.search.input.toLowerCase();
if (search) {
local.search.result = options.filter((e) =>
e.label.toLowerCase().includes(search)
);
if (
local.search.result.length > 0 &&
!local.search.result.find(
(e) => e.value === local.select?.value
)
) {
local.select = local.search.result[0];
}
} else {
local.search.result = null;
}
local.search.searching = false;
local.render();
} else {
clearTimeout(local.search.timeout);
local.search.timeout = setTimeout(async () => {
const result = options_fn?.({
search: local.search.input,
existing: options,
});
if (result) {
if (result instanceof Promise) {
local.search.promise = result;
local.search.result = (await result).map((item) => {
if (typeof item === "string")
return { value: item, label: item };
return item;
});
local.search.searching = false;
local.search.promise = null;
} else {
local.search.result = result.map((item) => {
if (typeof item === "string")
return { value: item, label: item };
return item;
});
local.search.searching = false;
}
if (
local.search.result.length > 0 &&
!local.search.result.find(
(e) => e.value === local.select?.value
)
) {
local.select = local.search.result[0];
}
local.render();
}
}, 100);
}
}
}}
disabled={!disabled ? disabledSearch : disabled}
spellCheck={false}
className={cx(
"text-black flex h-9 w-full border-input bg-transparent px-3 py-1 text-base border-none shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0",
local.mode === "single" ? "cursor-pointer" : ""
)}
style={{
pointerEvents: disabledSearch ? "none" : "auto", // Mencegah input menangkap klik saat disabled
}}
onKeyDown={keydown}
/>
</div>
</TypeaheadOptions>
</div>
{local.mode === "single" && (
<>
<div
className={cx(
"typeahead-arrow absolute z-10 inset-0 left-auto flex items-center ",
" justify-center w-6 mr-1 my-2 bg-transparant",
disabled ? "hidden" : "cursor-pointer"
)}
onClick={() => {
if (!disabled) {
local.value = [];
local.render();
if (typeof onChange === "function") onChange(local.value);
}
}}
>
{inputval ? <X size={14} /> : <GoChevronDown size={14} />}
</div>
</>
)}
</div>
);
};

View File

@ -0,0 +1,130 @@
import { FC } from "react";
import { useLocal } from "@/lib/utils/use-local";
import { Popover } from "../../Popover/Popover";
export type OptionItem = { value: string; label: string };
export const TypeaheadOptions: FC<{
popup?: boolean;
open?: boolean;
children: any;
onOpenChange?: (open: boolean) => void;
options: OptionItem[];
className?: string;
showEmpty: boolean;
selected?: (arg: {
item: OptionItem;
options: OptionItem[];
idx: number;
}) => boolean;
onSelect?: (value: string) => void;
searching?: boolean;
searchText?: string;
width?: number;
isMulti?: boolean
}> = ({
popup,
children,
open,
onOpenChange,
className,
options,
selected,
onSelect,
searching,
searchText,
showEmpty,
width,isMulti
}) => {
if (!popup) return children;
const local = useLocal({
selectedIdx: 0,
});
let content = (
<div
className={cx(
className,
width
? css`
min-width: ${width}px;
`
: css`
min-width: 150px;
`,
css`
max-height: 400px;
overflow: auto
`
)}
>
{options.map((item, idx) => {
const is_selected = selected?.({ item, options, idx });
if (is_selected) {
local.selectedIdx = idx;
}
return (
<div
tabIndex={0}
key={item.value + "_" + idx}
className={cx(
"opt-item px-3 py-1 cursor-pointer option-item text-sm",
is_selected ? "bg-blue-600 text-white" : "hover:bg-blue-50",
idx > 0 && "border-t"
)}
onClick={() => {
onSelect?.(item.value);
}}
>
{item.label || <>&nbsp;</>}
</div>
);
})}
{searching ? (
<div className="px-4 w-full text-slate-400">Loading...</div>
) : (
<>
{options.length === 0 && (
<div className="p-4 w-full text-center text-md text-slate-400">
{!searchText ? (
<>&mdash; Empty &mdash;</>
) : (
<>
Search
<span
className={css`
font-style: italic;
padding: 0px 5px;
`}
>
"{searchText}"
</span>
not found
</>
)}
</div>
)}
</>
)}
</div>
);
if (!showEmpty && options.length === 0) content = <></>;
return (
<Popover
open={open}
arrow={false}
onOpenChange={onOpenChange}
backdrop={false}
classNameTrigger={!isMulti ? "w-full" : ""}
placement="bottom-start"
className="flex-1 rounded-md overflow-hidden"
content={content}
>
{children}
</Popover>
);
};

View File

@ -0,0 +1,141 @@
'use client';
const Footer: React.FC = () => {
return (
<>
<footer className="p-6 my-6 mx-4 bg-white rounded-2xl shadow-lg shadow-gray-200 md:flex md:items-center md:justify-between">
<ul className="flex flex-wrap items-center mb-6 md:mb-0">
<li>
<a
href="#"
className="mr-4 text-md font-normal text-gray-500 hover:underline md:mr-6"
>
Terms and conditions
</a>
</li>
<li>
<a
href="#"
className="mr-4 text-md font-normal text-gray-500 hover:underline md:mr-6"
>
Privacy Policy
</a>
</li>
<li>
<a
href="#"
className="mr-4 text-md font-normal text-gray-500 hover:underline md:mr-6"
>
Licensing
</a>
</li>
<li>
<a
href="#"
className="mr-4 text-md font-normal text-gray-500 hover:underline md:mr-6"
>
Cookie Policy
</a>
</li>
<li>
<a
href="#"
className="text-md font-normal text-gray-500 hover:underline"
>
Contact
</a>
</li>
</ul>
<div className="flex space-x-6 sm:justify-center">
<a href="#" className="text-gray-500 hover:text-gray-900">
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z"
clipRule="evenodd"
/>
</svg>
</a>
<a href="#" className="text-gray-500 hover:text-gray-900">
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z"
clipRule="evenodd"
/>
</svg>
</a>
<a href="#" className="text-gray-500 hover:text-gray-900">
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
</svg>
</a>
<a href="#" className="text-gray-500 hover:text-gray-900">
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
clipRule="evenodd"
/>
</svg>
</a>
<a href="#" className="text-gray-500 hover:text-gray-900">
<svg
className="w-5 h-5"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2zm6.605 4.61a8.502 8.502 0 011.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.416 25.416 0 00-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362zM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.687 8.687 0 0112 3.475zm-3.633.803a53.896 53.896 0 013.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 014.729-5.975zM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.522 8.522 0 01-2.19-5.705zM12 20.547a8.482 8.482 0 01-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.318 35.318 0 011.823 6.475 8.4 8.4 0 01-3.341.684zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 01-3.655 5.715z"
clipRule="evenodd"
/>
</svg>
</a>
</div>
</footer>
<p className="my-10 text-md text-center text-gray-500">
&copy; 2019-2023 Built with by
<a
href="https://creative-tim.com"
className="hover:underline"
target="_blank"
>
Creative Tim
</a>
and
<a
href="https://flowbite.com"
className="hover:underline"
target="_blank"
>
Flowbite
</a>
. All rights reserved.
</p>
</>
);
};
export default Footer;

View File

@ -0,0 +1,45 @@
"use client";
import React from "react";
interface HeaderProps {
title: string;
description?: string;
author?: string;
robots?: string;
}
const Header: React.FC<HeaderProps> = ({
title,
description,
author,
robots,
}) => {
return (
<>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{description && <meta name="description" content={description} />}
{author && <meta name="author" content={author} />}
<meta name="generator" content="Next.js" />
<title>{title}</title>
<link
rel="canonical"
href="https://www.creative-tim.com/product/soft-ui-dashboard-pro-flowbite"
/>
{robots && <meta name="robots" content={robots} />}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"
/>
</>
);
};
export default Header;

View File

@ -0,0 +1,445 @@
"use client";
import React, { FC } from "react";
import {
Avatar,
DarkThemeToggle,
Dropdown,
Label,
Navbar,
TextInput,
} from "flowbite-react";
import {
HiArchive,
HiBell,
HiCog,
HiCurrencyDollar,
HiEye,
HiInbox,
HiLogout,
HiMenuAlt1,
HiOutlineTicket,
HiSearch,
HiShoppingBag,
HiUserCircle,
HiUsers,
HiViewGrid,
HiX,
} from "react-icons/hi";
import { siteurl } from "@/lib/utils/siteurl";
import { get_user } from "@/lib/utils/get_user";
import api from "@/lib/utils/axios";
const NavFlow: React.FC<any> = ({ minimaze }) => {
return (
<Navbar fluid>
<div className="w-full p-1 lg:px-5 lg:pl-3">
<div className="flex items-center justify-between">
<div className="flex items-center">
{true && (
<button
onClick={minimaze}
className="mr-3 cursor-pointer rounded p-2 text-gray-600 hover:bg-gray-100 hover:text-gray-900 lg:inline"
>
<span className="sr-only">Toggle sidebar</span>
<HiMenuAlt1 className="h-6 w-6" />
</button>
)}
<Navbar.Brand href="/">
<img
alt=""
src={siteurl("/julong.png")}
className="mr-3 h-6 sm:h-8"
/>
<span className="self-center whitespace-nowrap text-2xl font-semibold text-black">
Man Power Management
</span>
</Navbar.Brand>
</div>
<div className="flex items-center lg:gap-3">
<div className="flex items-center">
<NotificationBellDropdown />
</div>
<div className="hidden lg:block">
<UserDropdown />
</div>
</div>
</div>
</div>
</Navbar>
);
};
const NotificationBellDropdown: FC = function () {
return (
<Dropdown
arrowIcon={false}
inline
label={
<span className="rounded-lg p-2 hover:bg-gray-100 ">
<span className="sr-only">Notifications</span>
<HiBell className="text-2xl text-gray-500 hover:text-gray-900 " />
</span>
}
>
<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>
<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="shrink-0">
<img
alt=""
src={siteurl("/dog.jpg")}
className="h-11 w-11 rounded-full"
/>
<div className="absolute -mt-5 ml-6 flex h-5 w-5 items-center justify-center rounded-full border border-white bg-primary dark:border-gray-700">
<NewMessageIcon />
</div>
</div>
<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-b py-3 px-4 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="shrink-0">
<img
alt=""
src={siteurl("/dog.jpg")}
className="h-11 w-11 rounded-full"
/>
<div className="absolute -mt-5 ml-6 flex h-5 w-5 items-center justify-center rounded-full border border-white bg-gray-900 dark:border-gray-700">
<NewFollowIcon />
</div>
</div>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
<span className="font-semibold text-gray-900 ">Jese Leos</span>
&nbsp;and&nbsp;
<span className="font-medium text-gray-900 ">5 others</span>
&nbsp;started following you.
</div>
<div className="text-md font-medium text-primary-700 ">
10 minutes ago
</div>
</div>
</a>
<a
href="#"
className="flex border-b py-3 px-4 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="shrink-0">
<img
alt=""
src={siteurl("/dog.jpg")}
className="h-11 w-11 rounded-full"
/>
<div className="absolute -mt-5 ml-6 flex h-5 w-5 items-center justify-center rounded-full border border-white bg-red-600 dark:border-gray-700">
<NewLoveIcon />
</div>
</div>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
<span className="font-semibold text-gray-900 ">
Joseph Mcfall
</span>
&nbsp;and&nbsp;
<span className="font-medium text-gray-900 ">141 others</span>
&nbsp;love your story. See it and view more stories.
</div>
<div className="text-md font-medium text-primary-700 ">
44 minutes ago
</div>
</div>
</a>
<a
href="#"
className="flex border-b py-3 px-4 hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-600"
>
<div className="shrink-0">
<img
alt=""
src={siteurl("/dog.jpg")}
className="h-11 w-11 rounded-full"
/>
<div className="absolute -mt-5 ml-6 flex h-5 w-5 items-center justify-center rounded-full border border-white bg-green-400 dark:border-gray-700">
<NewMentionIcon />
</div>
</div>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
<span className="font-semibold text-gray-900 ">
Leslie Livingston
</span>
&nbsp;mentioned you in a comment:&nbsp;
<span className="font-medium text-primary-700 ">
@bonnie.green
</span>
&nbsp;what do you say?
</div>
<div className="text-md font-medium text-primary-700 ">
1 hour ago
</div>
</div>
</a>
<a
href="#"
className="flex py-3 px-4 hover:bg-gray-100 dark:hover:bg-gray-600"
>
<div className="shrink-0">
<img
alt=""
src={siteurl("/dog.jpg")}
className="h-11 w-11 rounded-full"
/>
<div className="absolute -mt-5 ml-6 flex h-5 w-5 items-center justify-center rounded-full border border-white bg-purple-500 dark:border-gray-700">
<NewVideoIcon />
</div>
</div>
<div className="w-full pl-3">
<div className="mb-1.5 text-md font-normal text-gray-500 ">
<span className="font-semibold text-gray-900 ">
Robert Brown
</span>
&nbsp;posted a new video: Glassmorphism - learn how to implement
the new design trend.
</div>
<div className="text-md font-medium text-primary-700 ">
3 hours 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>
</Dropdown>
);
};
const NewMessageIcon: FC = function () {
return (
<svg
className="h-3 w-3 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l2-2a1 1 0 00-1.414-1.414L11 7.586V3a1 1 0 10-2 0v4.586l-.293-.293z"></path>
<path d="M3 5a2 2 0 012-2h1a1 1 0 010 2H5v7h2l1 2h4l1-2h2V5h-1a1 1 0 110-2h1a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5z"></path>
</svg>
);
};
const NewFollowIcon: FC = function () {
return (
<svg
className="h-3 w-3 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M8 9a3 3 0 100-6 3 3 0 000 6zM8 11a6 6 0 016 6H2a6 6 0 016-6zM16 7a1 1 0 10-2 0v1h-1a1 1 0 100 2h1v1a1 1 0 102 0v-1h1a1 1 0 100-2h-1V7z"></path>
</svg>
);
};
const NewLoveIcon: FC = function () {
return (
<svg
className="h-3 w-3 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z"
clipRule="evenodd"
></path>
</svg>
);
};
const NewMentionIcon: FC = function () {
return (
<svg
className="h-3 w-3 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M18 13V5a2 2 0 00-2-2H4a2 2 0 00-2 2v8a2 2 0 002 2h3l3 3 3-3h3a2 2 0 002-2zM5 7a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1zm1 3a1 1 0 100 2h3a1 1 0 100-2H6z"
clipRule="evenodd"
></path>
</svg>
);
};
const NewVideoIcon: FC = function () {
return (
<svg
className="h-3 w-3 text-white"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M2 6a2 2 0 012-2h6a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zM14.553 7.106A1 1 0 0014 8v4a1 1 0 00.553.894l2 1A1 1 0 0018 13V7a1 1 0 00-1.447-.894l-2 1z"></path>
</svg>
);
};
const AppDrawerDropdown: FC = function () {
return (
<Dropdown
arrowIcon={false}
inline
label={
<span className="rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-700">
<span className="sr-only">Apps</span>
<HiViewGrid className="text-2xl text-gray-500 hover:text-gray-900 dark:hover:text-white" />
</span>
}
>
<div className="block rounded-t-lg border-b bg-gray-50 py-2 px-4 text-center text-base font-medium text-gray-700 dark:border-b-gray-600 dark:bg-gray-700 ">
Apps
</div>
<div className="grid grid-cols-3 gap-4 p-4">
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiShoppingBag className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Sales</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiUsers className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Users</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiInbox className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Inbox</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiUserCircle className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Profile</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiCog className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Settings</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiArchive className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Products</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiCurrencyDollar className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Pricing</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiOutlineTicket className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Billing</div>
</a>
<a
href="#"
className="block rounded-lg p-4 text-center hover:bg-gray-100 dark:hover:bg-gray-600"
>
<HiLogout className="mx-auto mb-1 h-7 w-7 text-gray-500 " />
<div className="text-md font-medium text-gray-900 ">Logout</div>
</a>
</div>
</Dropdown>
);
};
const UserDropdown: FC = function () {
return (
<Dropdown
arrowIcon={false}
inline
label={
<span>
<span className="sr-only">User menu</span>
<Avatar alt="" img={siteurl("/dog.jpg")} rounded size="sm" />
</span>
}
>
<Dropdown.Header>
<span className="block text-md">
{get_user("employee.name") ? get_user("employee.name") : "-"}
</span>
<span className="block truncate text-md font-medium">
{get_user("employee.email") ? get_user("employee.email") : "-"}
</span>
</Dropdown.Header>
<Dropdown.Item
onClick={() => {
if (typeof window === "object")
navigate(
`${process.env.NEXT_PUBLIC_API_PORTAL}/choose-roles?state=manpower`
);
}}
>
Switch Role
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
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`);
}}
>
Sign out
</Dropdown.Item>
</Dropdown>
);
};
export default NavFlow;

View File

@ -0,0 +1,9 @@
'use client';
const Script = () => {
return (
<script async defer src="https://buttons.github.io/buttons.js"></script>
);
};
export default Script;

View File

@ -0,0 +1,488 @@
"use client";
import React, { FC } from "react";
import Link from "next/link";
import { Dropdown, Sidebar, TextInput, Tooltip } from "flowbite-react";
import { useEffect, useState } from "react";
import { useSidebarContext } from "@/context/SidebarContext";
import classNames from "classnames";
import { HiAdjustments, HiChartPie, HiCog } from "react-icons/hi";
import isSmallScreen from "@/helpers/is-small-screen";
import { css } from "@emotion/css";
import { FaAngleUp, FaChevronDown, FaChevronUp } from "react-icons/fa";
import { Minimize } from "lucide-react";
import { SidebarLinkBetter } from "../ui/link-better";
import { detectCase } from "@/utils/detectCase";
import { Skeleton } from "../ui/Skeleton";
import { useLocal } from "@/lib/utils/use-local";
interface TreeMenuItem {
title: string;
href?: string;
children?: TreeMenuItem[];
icon?: any;
}
interface TreeMenuProps {
data: TreeMenuItem[];
minimaze: () => void;
mini: boolean;
}
const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
const [currentPage, setCurrentPage] = useState("");
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;
const isActive = item.href && detectCase(currentPage, item.href);
const isParentActive = hasChildren && isChildActive(item.children!);
const [isOpen, setIsOpen] = useState(isParentActive);
useEffect(() => {
if (isParentActive) {
setIsOpen(true);
}
}, [isParentActive]);
const itemStyle = {
paddingLeft: !mini ? `${depth * 16}px` : "0px",
};
return (
<React.Fragment key={item.href || item.title || index}>
{hasChildren ? (
<li>
<div
className={classNames(
" flex-row flex items-center cursor-pointer items-center w-full rounded-lg text-base font-normal text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-700 flex flex-row",
isParentActive && !depth
? " text-base font-normal text-dark-500 rounded-lg hover:bg-gray-200 group bg-white shadow-md shadow-[#31367875] hover:!bg-white transition-all duration-200 dark:bg-gray-700"
: " ",
mini ? "m-0 flex-grow w-full" : "py-2.5 px-4 ",
mini
? css`
margin: 0 !important;
`
: ""
)}
onClick={() => {
if (mini) {
minimaze();
}
setIsOpen(!isOpen);
}}
style={itemStyle}
>
<div
className={cx(
"flex flex-row items-center flex-grow",
mini ? "py-2 justify-center rounded-lg" : " px-3",
mini
? isParentActive
? "bg-[#313678]"
: "bg-white hover:bg-gray-300 shadow shadow-gray-300"
: ""
)}
>
{!depth ? (
<div
className={classNames(
" w-8 h-8 rounded-lg text-center flex flex-row items-center justify-center",
isParentActive
? "bg-[#313678] text-white active-menu-icon"
: "bg-white shadow-lg text-black",
!mini
? "mr-1 p-2 shadow-gray-300"
: " text-lg shadow-none",
mini
? css`
background: transparent !important;
`
: ``
)}
>
{item.icon}
</div>
) : (
<></>
)}
{!mini ? (
<>
<div className="pl-2 flex-grow text-black text-xs">
{item.title}
</div>
<div className="text-md">
{isOpen ? <FaChevronUp /> : <FaChevronDown />}
</div>
</>
) : (
<></>
)}
</div>
</div>
<Sidebar.ItemGroup
className={classNames(
"border-none mt-0",
isOpen ? "" : "hidden",
mini ? "hidden" : ""
)}
>
{renderTree(item.children!, depth + 1)}
</Sidebar.ItemGroup>
</li>
) : (
<li>
<SidebarLinkBetter
href={item.href}
onClick={() => {
if (item?.href) setCurrentPage(item.href);
}}
className={classNames(
" flex-row flex items-center cursor-pointer items-center w-full rounded-lg text-base font-normal text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-700 flex flex-row py-2.5 px-4",
isActive
? " py-2.5 px-4 text-base font-normal text-dark-500 rounded-lg group shadow-[#31367875] transition-all duration-200 dark:bg-gray-700"
: "",
isActive
? !depth
? " bg-white shadow-md hover:bg-gray-200 hover:!bg-white "
: "bg-gray-100"
: "",
css`
& > span {
white-space: wrap !important;
}
`,
mini ? "px-0 py-2" : ""
)}
style={itemStyle} // Terapkan gaya berdasarkan depth
>
<div className="flex flex-row items-center">
{!depth ? (
<div
className={classNames(
" shadow-gray-300 text-dark-700 w-8 h-8 rounded-lg text-center flex flex-row items-center justify-center shadow-[#313678]",
isActive
? "bg-[#313678] text-white"
: "bg-white shadow-lg text-black",
!mini ? "mr-1 p-2" : " text-lg"
)}
>
{item.icon}
</div>
) : (
<></>
)}
{!mini ? (
<>
<div className="pl-2 text-black text-xs">
{item.title}
</div>
</>
) : (
<></>
)}
</div>
</SidebarLinkBetter>
</li>
)}
</React.Fragment>
);
});
};
return (
<div className={classNames("flex h-full lg:!block", {})}>
<Sidebar
aria-label="Sidebar with multi-level dropdown example"
className={classNames("relative bg-white", mini ? "w-20" : "", css``)}
>
{/* {!local.ready ? (
<div
className={cx(
"absolute",
css`
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
`
)}
>
<div className="flex flex-grow flex-row items-center justify-center">
<div className="flex flex-col gap-y-2">
<div className="flex flex-row gap-x-2">
<Skeleton className="h-24 flex-grow" />
<Skeleton className="h-24 flex-grow" />
</div>
<Skeleton className="h-24 w-[230px]" />
<div className="flex flex-row gap-x-2">
<Skeleton className="h-24 flex-grow" />
<Skeleton className="h-24 flex-grow" />
</div>
<Skeleton className="h-24 w-[230px]" />
</div>
</div>
</div>
) : (
)} */}
<div className="w-full h-full relative">
<div className="flex h-full flex-col justify-between w-full absolute top-0 left-0">
<Sidebar.Items>
<Sidebar.ItemGroup
className={cx(
"border-none mt-0",
mini ? "flex flex-col gap-y-2" : ""
)}
>
{renderTree(data)}
</Sidebar.ItemGroup>
</Sidebar.Items>
</div>
</div>
</Sidebar>
</div>
);
};
const BottomMenu: FC = function () {
return (
<div className="flex items-center justify-center gap-x-5">
<button className="rounded-lg p-2 hover:bg-gray-100">
<span className="sr-only">Tweaks</span>
<HiAdjustments className="text-2xl text-gray-500 hover:text-gray-900 " />
</button>
<div>
<Tooltip content="Settings page">
<a
href="/users/settings"
className="inline-flex cursor-pointer justify-center rounded p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900"
>
<span className="sr-only">Settings page</span>
<HiCog className="text-2xl text-gray-500 hover:text-gray-900 " />
</a>
</Tooltip>
</div>
<div>
<LanguageDropdown />
</div>
</div>
);
};
const LanguageDropdown: FC = function () {
return (
<Dropdown
arrowIcon={false}
inline
label={
<span className="inline-flex cursor-pointer justify-center rounded p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-900 dark:hover:bg-gray-700 dark:hover:text-white">
<span className="sr-only">Current language</span>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
viewBox="0 0 3900 3900"
className="h-5 w-5 rounded-full"
>
<path fill="#b22234" d="M0 0h7410v3900H0z"></path>
<path
d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0"
stroke="#fff"
strokeWidth="300"
></path>
<path fill="#3c3b6e" d="M0 0h2964v2100H0z"></path>
<g fill="#fff">
<g id="d">
<g id="c">
<g id="e">
<g id="b">
<path
id="a"
d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"
></path>
<use xlinkHref="#a" y="420"></use>
<use xlinkHref="#a" y="840"></use>
<use xlinkHref="#a" y="1260"></use>
</g>
<use xlinkHref="#a" y="1680"></use>
</g>
<use xlinkHref="#b" x="247" y="210"></use>
</g>
<use xlinkHref="#c" x="494"></use>
</g>
<use xlinkHref="#d" x="988"></use>
<use xlinkHref="#c" x="1976"></use>
<use xlinkHref="#e" x="2470"></use>
</g>
</svg>
</span>
}
>
<ul className="py-1" role="none">
<li>
<a
href="#"
className="block py-2 px-4 text-md text-gray-700 hover:bg-gray-100 "
>
<div className="inline-flex items-center">
<svg
className="mr-2 h-4 w-4 rounded-full"
xmlns="http://www.w3.org/2000/svg"
id="flag-icon-css-us"
viewBox="0 0 512 512"
>
<g fillRule="evenodd">
<g strokeWidth="1pt">
<path
fill="#bd3d44"
d="M0 0h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0z"
transform="scale(3.9385)"
/>
<path
fill="#fff"
d="M0 10h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0z"
transform="scale(3.9385)"
/>
</g>
<path
fill="#192f5d"
d="M0 0h98.8v70H0z"
transform="scale(3.9385)"
/>
<path
fill="#fff"
d="M8.2 3l1 2.8H12L9.7 7.5l.9 2.7-2.4-1.7L6 10.2l.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7L74 8.5l-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 7.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 24.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 21.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 38.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 35.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 52.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 49.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 66.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 63.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9z"
transform="scale(3.9385)"
/>
</g>
</svg>
<span className="whitespace-nowrap">English (US)</span>
</div>
</a>
</li>
<li>
<a
href="#"
className="block py-2 px-4 text-md text-gray-700 hover:bg-gray-100"
>
<div className="inline-flex items-center">
<svg
className="mr-2 h-4 w-4 rounded-full"
xmlns="http://www.w3.org/2000/svg"
id="flag-icon-css-de"
viewBox="0 0 512 512"
>
<path fill="#ffce00" d="M0 341.3h512V512H0z" />
<path d="M0 0h512v170.7H0z" />
<path fill="#d00" d="M0 170.7h512v170.6H0z" />
</svg>
Deutsch
</div>
</a>
</li>
<li>
<a
href="#"
className="block py-2 px-4 text-md text-gray-700 hover:bg-gray-100 "
>
<div className="inline-flex items-center">
<svg
className="mr-2 h-4 w-4 rounded-full"
xmlns="http://www.w3.org/2000/svg"
id="flag-icon-css-it"
viewBox="0 0 512 512"
>
<g fillRule="evenodd" strokeWidth="1pt">
<path fill="#fff" d="M0 0h512v512H0z" />
<path fill="#009246" d="M0 0h170.7v512H0z" />
<path fill="#ce2b37" d="M341.3 0H512v512H341.3z" />
</g>
</svg>
Italiano
</div>
</a>
</li>
<li>
<a
href="#"
className="block py-2 px-4 text-md text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
<div className="inline-flex items-center">
<svg
className="mr-2 h-4 w-4 rounded-full"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
id="flag-icon-css-cn"
viewBox="0 0 512 512"
>
<defs>
<path id="a" fill="#ffde00" d="M1-.3L-.7.8 0-1 .6.8-1-.3z" />
</defs>
<path fill="#de2910" d="M0 0h512v512H0z" />
<use
width="30"
height="20"
transform="matrix(76.8 0 0 76.8 128 128)"
xlinkHref="#a"
/>
<use
width="30"
height="20"
transform="rotate(-121 142.6 -47) scale(25.5827)"
xlinkHref="#a"
/>
<use
width="30"
height="20"
transform="rotate(-98.1 198 -82) scale(25.6)"
xlinkHref="#a"
/>
<use
width="30"
height="20"
transform="rotate(-74 272.4 -114) scale(25.6137)"
xlinkHref="#a"
/>
<use
width="30"
height="20"
transform="matrix(16 -19.968 19.968 16 256 230.4)"
xlinkHref="#a"
/>
</svg>
<span className="whitespace-nowrap"> ()</span>
</div>
</a>
</li>
</ul>
</Dropdown>
);
};
export default SidebarTree;

View File

@ -0,0 +1,702 @@
"use client";
import { makeData } from "@/lib/utils/makeData";
import {
ColumnDef,
ColumnResizeDirection,
ColumnResizeMode,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { FC, useCallback, useEffect, useState } from "react";
import {
Breadcrumb,
Button,
Checkbox,
Label,
Modal,
Table,
TextInput,
} from "flowbite-react";
import {
HiChevronLeft,
HiChevronRight,
HiHome,
HiOutlinePencilAlt,
HiPlus,
HiSearch,
HiTrash,
} from "react-icons/hi";
import classNames from "classnames";
import { useLocal } from "@/lib/utils/use-local";
import { debouncedHandler } from "@/lib/utils/debounceHandler";
import { FaArrowDownLong, FaArrowUp, FaChevronUp } from "react-icons/fa6";
import Link from "next/link";
import { init_column } from "./lib/column";
import { toast } from "sonner";
import { Check, Loader2, Sticker } from "lucide-react";
import { InputSearch } from "../ui/input-search";
import { Input } from "../ui/input";
import { FaChevronDown } from "react-icons/fa";
import get from "lodash.get";
export const TableList: React.FC<any> = ({
name,
column,
onLoad,
take = 50,
header,
disabledPagination,
disabledHeader,
disabledHeadTable,
hiddenNoRow,
disabledHoverRow,
onInit,
}) => {
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 local = useLocal({
table: null as any,
data: [] as any[],
sort: {} as any,
search: null as any,
addRow: (row: any) => {
setData((prev) => [...prev, row]);
local.data.push(row);
local.render();
},
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;
local.render();
setData(e);
setTimeout(() => {
toast.dismiss();
}, 2000);
});
} else {
local.data = res;
local.render();
setData(res);
setTimeout(() => {
toast.dismiss();
}, 2000);
}
}
},
});
useEffect(() => {
if (typeof onInit === "function") {
onInit(local);
}
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;
local.render();
setData(e);
setTimeout(() => {
toast.dismiss();
}, 2000);
});
} else {
local.data = res;
local.render();
setData(res);
setTimeout(() => {
toast.dismiss();
}, 2000);
}
}
}, []);
const defaultColumns: ColumnDef<Person>[] = init_column(column);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columns] = React.useState<typeof 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 table = useReactTable({
data: data,
columnResizeMode,
columnResizeDirection,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
initialState: {
pagination: {
pageIndex: 0, //custom initial page index
pageSize: 25, //custom default page size
},
},
state: {
pagination: {
pageIndex: 0,
pageSize: 50,
},
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), // 1 detik jeda
[]
);
return (
<>
<div className="tbl-wrapper flex flex-grow flex-col">
{!disabledHeader ? (
<div className="head-tbl-list block items-start justify-between border-b border-gray-200 bg-white p-4 sm:flex">
<div className="flex flex-row items-end">
<div className="sm:flex flex flex-col space-y-2">
{false ? (
<div className="">
<h2 className="text-xl font-semibold text-gray-900 sm:text-2xl">
All <span className="">{name ? `${name}s` : ``}</span>
</h2>
</div>
) : (
<></>
)}
<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="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="min-w-full divide-y divide-gray-200 ">
{!disabledHeadTable ? (
<thead className="text-md bg-second group/head text-md uppercase text-gray-700 sticky top-0">
{table.getHeaderGroups().map((headerGroup) => (
<tr
key={`${headerGroup.id}`}
className={headerGroup.id}
>
{headerGroup.headers.map((header, index) => {
const name = header.column.id;
const col = column.find(
(e: any) => e?.name === name
);
const isSort =
typeof col?.sortable === "boolean"
? col.sortable
: true;
return (
<th
{...{
style: {
width: col?.width
? header.getSize() < col?.width
? `${col.width}px`
: header.getSize()
: header.getSize(),
},
}}
key={header.id}
colSpan={header.colSpan}
className="relative px-2 py-2 text-sm py-1 "
>
<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="flex flex-row items-center flex-grow text-sm">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</div>
{isSort ? (
<div className="flex flex-col items-center">
<FaChevronUp
className={cx(
"px-0.5 mx-1 text-[12px]",
local?.sort?.[name] === "asc"
? "text-black"
: "text-gray-500"
)}
/>
<FaChevronDown
className={cx(
"px-0.5 mx-1 text-[12px]",
local?.sort?.[name] === "desc"
? "text-black"
: "text-gray-500"
)}
/>
</div>
) : (
<></>
)}
</div>
{headerGroup.headers.length !== index + 1 ? (
<div
key={`${header.id}-resizer`} // Tambahkan key unik
{...{
onDoubleClick: () =>
header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
className: `resizer w-0.5 bg-gray-300 ${
table.options.columnResizeDirection
} ${
header.column.getIsResizing()
? "isResizing"
: ""
}`,
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 divide-gray-200 bg-white">
{table.getRowModel().rows.map((row, idx) => (
<Table.Row
key={row.id}
className={cx(
disabledHoverRow ? "" : "hover:bg-[#DBDBE7]",
css`
height: 44px;
`
)}
>
{row.getVisibleCells().map((cell) => {
const ctx = cell.getContext();
const param = {
row: row.original,
name: get(ctx, "column.columnDef.accessorKey"),
cell,
idx,
tbl: local,
};
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 "
)}
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>
<Pagination
onNextPage={() => table.nextPage()}
onPrevPage={() => table.previousPage()}
disabledNextPage={!table.getCanNextPage()}
disabledPrevPage={!table.getCanPreviousPage()}
page={table.getState().pagination.pageIndex + 1}
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,
countPage,
countData,
take,
onChangePage,
}) => {
const local = useLocal({
page: 1 as any,
});
useEffect(() => {
local.page = page;
local.render();
}, [page]);
return (
<div className="tbl-pagination sticky text-sm bottom-0 right-0 w-full items-center justify-end text-sm border-t border-gray-200 bg-white p-4 sm:flex">
<div className="mb-4 flex items-center sm:mb-0">
<div
onClick={() => {
if (!disabledPrevPage) {
onPrevPage();
}
}}
className={classNames(
"inline-flex justify-center rounded p-1 ",
disabledPrevPage
? "text-gray-200"
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900"
)}
>
<span className="sr-only">Previous page</span>
<HiChevronLeft className="text-2xl" />
</div>
<div
onClick={() => {
if (!disabledNextPage) {
onNextPage();
}
}}
className={classNames(
"inline-flex justify-center rounded p-1 ",
disabledNextPage
? "text-gray-200"
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900"
)}
>
<span className="sr-only">Next page</span>
<HiChevronRight className="text-2xl" />
</div>
<span className="text-md font-normal text-gray-500">
Page&nbsp;
<span className="font-semibold text-gray-900">{page}</span>
&nbsp;of&nbsp;
<span className="font-semibold text-gray-900">{countPage}</span>
</span>
<span className="flex items-center pl-2 text-black gap-x-2">
| Go to page:
<form
onSubmit={(e) => {
e.preventDefault();
const page = Number(local.page);
if (!page) {
local.page = 0;
} else if (page > countPage) {
local.page = countPage;
}
local.render();
onChangePage(local.page - 1);
}}
>
<Input
type="number"
min="1"
max={countPage}
value={local.page}
onChange={(e) => {
local.page = e.target.value;
local.render();
debouncedHandler(() => {
const page = Number(local.page);
if (!page) {
local.page = 0;
} else if (page > countPage) {
local.page = countPage;
}
local.render();
onChangePage(local.page - 1);
}, 1500);
}}
/>
</form>
</span>
</div>
<div className="flex items-center space-x-3 hidden">
{!disabledPrevPage ? (
<>
<div
onClick={() => {
if (!disabledPrevPage) {
onPrevPage();
}
}}
className={classNames(
"cursor-pointer inline-flex flex-1 items-center justify-center rounded-lg bg-primary px-3 py-2 text-center text-md font-medium text-white hover:bg-primary focus:ring-4 focus:ring-primary-300"
)}
>
<HiChevronLeft className="mr-1 text-base" />
Previous
</div>
</>
) : (
<></>
)}
{!disabledNextPage ? (
<>
<div
onClick={() => {
if (!disabledNextPage) {
onNextPage();
}
}}
className={classNames(
"cursor-pointer inline-flex flex-1 items-center justify-center rounded-lg bg-primary px-3 py-2 text-center text-md font-medium text-white hover:bg-primary focus:ring-4 focus:ring-primary-300"
)}
>
Next
<HiChevronRight className="ml-1 text-base" />
</div>
</>
) : (
<></>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,10 @@
export const init_column = (data: any[]) => {
return data.length
? data.map((e) => {
return {
accessorKey: e.name,
...e,
};
})
: [];
};

View File

@ -0,0 +1,282 @@
"use client";
import { makeData } from "@/lib/utils/makeData";
import {
ColumnDef,
ColumnResizeDirection,
ColumnResizeMode,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { FC, useEffect } from "react";
import {
Breadcrumb,
Button,
Checkbox,
Label,
Modal,
Table,
TextInput,
} from "flowbite-react";
import { useLocal } from "@/lib/utils/use-local";
import { FaArrowDownLong, FaArrowUp } from "react-icons/fa6";
import { init_column } from "../tablelist/lib/column";
export const List: React.FC<any> = ({
name,
column,
onLoad,
take = 20,
header,
}) => {
const sideRight =
typeof header?.sideRight === "function" ? header.sideRight : null;
type Person = {
firstName: string;
lastName: string;
age: number;
visits: number;
status: string;
progress: number;
};
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(() => {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take: 10,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
local.render();
});
} else {
local.data = res;
local.render();
}
}, []);
const defaultColumns: ColumnDef<Person>[] = init_column(column);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columns] = React.useState<typeof defaultColumns>(() => [
...defaultColumns,
]);
const [columnResizeMode, setColumnResizeMode] =
React.useState<ColumnResizeMode>("onChange");
const [columnResizeDirection, setColumnResizeDirection] =
React.useState<ColumnResizeDirection>("ltr");
// Create the table and pass your options
const table = useReactTable({
data: local.data,
columnResizeMode,
columnResizeDirection,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
onSortingChange: setSorting,
state: {
pagination: {
pageIndex: 0,
pageSize: take,
},
sorting,
},
});
// 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,
}));
return (
<>
<div className="p-2 flex flex-grow flex-col">
<div className="flex flex-col flex-grow">
<div className="overflow-auto relative flex-grow flex-row">
<div className="absolute top-0 left-0 inline-block flex-grow w-full h-full align-middle">
<div className="shadow ">
<Table className="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
<thead className="bg-gray-100 dark:bg-gray-700 group/head text-md uppercase text-gray-700 bg-gray-100 dark:bg-gray-700">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={`${headerGroup.id}`} className={headerGroup.id}>
{headerGroup.headers.map((header, index) => {
const name = header.column.id;
const col = column.find((e: any) => e?.name === name);
const isSort =
typeof col?.sortable === "boolean"
? col.sortable
: true;
return (
<th
{...{
style: {
width:
col?.width && header.getSize() < col?.width
? col?.width
: header.getSize(),
},
}}
key={header.id}
colSpan={header.colSpan}
className="relative bg-gray-50 px-6 py-3 group-first/head:first:rounded-tl-lg group-first/head:last:rounded-tr-lg dark:bg-gray-700"
>
<div
key={`${header.id}-label`}
onClick={() => {
if (isSort) {
const sort = local?.sort?.[name];
const mode =
sort === "desc"
? null
: sort === "asc"
? "desc"
: "asc";
local.sort = mode
? {
[name]: mode,
}
: {};
local.render();
}
}}
className={cx(
"flex flex-grow flex-row select-none items-center flex-row",
isSort ? " cursor-pointer" : ""
)}
>
<div className="flex flex-row items-center">
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</div>
<div className="flex flex-row items-center">
{local?.sort?.[name] === "asc" ? (
<FaArrowUp className="px-0.5 mx-1" />
) : local?.sort?.[name] === "desc" ? (
<FaArrowDownLong className="px-0.5 mx-1" />
) : (
<></>
)}
</div>
</div>
{headerGroup.headers.length !== index + 1 ? (
<div
key={`${header.id}-resizer`} // Tambahkan key unik
{...{
onDoubleClick: () =>
header.column.resetSize(),
onMouseDown: header.getResizeHandler(),
onTouchStart: header.getResizeHandler(),
className: `resizer w-0.5 bg-gray-300 ${
table.options.columnResizeDirection
} ${
header.column.getIsResizing()
? "isResizing"
: ""
}`,
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 divide-gray-200 bg-white dark:divide-gray-700 dark:bg-gray-800">
{table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
className="hover:bg-gray-100 dark:hover:bg-gray-700"
>
{row.getVisibleCells().map((cell) => {
const ctx = cell.getContext();
const param = {
row: row.original,
name: ctx.column.id,
cell,
};
const head = column.find(
(e: any) => e?.name === ctx.column.id
);
const renderData =
typeof head?.renderCell === "function"
? head.renderCell(param)
: flexRender(
cell.column.columnDef.cell,
cell.getContext()
);
return (
<Table.Cell
className="whitespace-nowrap p-4 text-base font-medium text-gray-900 "
key={cell.id}
>
{renderData}
</Table.Cell>
);
})}
</Table.Row>
))}
</Table.Body>
</Table>
</div>
</div>
</div>
</div>
</div>
</>
);
};

View File

@ -0,0 +1,129 @@
"use client";
import { makeData } from "@/lib/utils/makeData";
import {
ColumnDef,
ColumnResizeDirection,
ColumnResizeMode,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import React, { FC, useEffect } from "react";
import {
Breadcrumb,
Button,
Checkbox,
Label,
Modal,
Table,
TextInput,
} from "flowbite-react";
import { useLocal } from "@/lib/utils/use-local";
import { FaArrowDownLong, FaArrowUp } from "react-icons/fa6";
import { init_column } from "../tablelist/lib/column";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import TabSlider from "../ui/tabslider";
export const Tablist: React.FC<any> = ({
name,
column,
onLabel,
onValue,
onLoad,
take = 20,
header,
tabContent,
disabledPagination,
}) => {
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(() => {
if (typeof onLoad === "function") {
const res: any = onLoad({
search: local.search,
sort: local.sort,
take: 10,
paging: 1,
});
if (res instanceof Promise) {
res.then((e) => {
local.data = e;
local.render();
});
} else {
local.data = res;
local.render();
}
} else {
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-gray-50 p-0 rounded-none">
<TabSlider className=" " disabledPagination={disabledPagination}>
{local.data.map((e, idx) => {
return (
<TabsTrigger
value={onValue(e)}
className={cx(
"p-1.5 px-4 border text-sm",
css`
z-index: -1;
`,
!idx ? "ml-1.5" : idx++ === local.data.length ? "mr-2" : ""
)}
key={onValue(e)}
>
{onLabel(e)}
</TabsTrigger>
);
})}
</TabSlider>
</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,467 @@
import dayjs from "dayjs";
import isBetween from "dayjs/plugin/isBetween";
import React, { useCallback, useContext } from "react";
import { BG_COLOR, TEXT_COLOR } from "../../constants";
import DatepickerContext from "../../contexts/DatepickerContext";
import {
formatDate,
nextMonth,
previousMonth,
classNames as cn,
} from "../../helpers";
import { Period } from "../../types";
dayjs.extend(isBetween);
interface Props {
calendarData: {
date: dayjs.Dayjs;
days: {
previous: number[];
current: number[];
next: number[];
};
};
onClickPreviousDays: (day: number) => void;
onClickDay: (day: number) => void;
onClickNextDays: (day: number) => void;
onIcon?: (day: number, date: Date) => any;
}
const Days: React.FC<Props> = ({
calendarData,
onClickPreviousDays,
onClickDay,
onClickNextDays,
onIcon,
}) => {
// Contexts
const {
primaryColor,
period,
changePeriod,
dayHover,
changeDayHover,
minDate,
maxDate,
disabledDates,
} = useContext(DatepickerContext);
// Functions
const currentDateClass = useCallback(
(item: number) => {
const itemDate = `${calendarData.date.year()}-${
calendarData.date.month() + 1
}-${item >= 10 ? item : "0" + item}`;
if (formatDate(dayjs()) === formatDate(dayjs(itemDate)))
return TEXT_COLOR["500"][
primaryColor as keyof (typeof TEXT_COLOR)["500"]
];
return "";
},
[calendarData.date, primaryColor]
);
const activeDateData = useCallback(
(day: number) => {
const fullDay = `${calendarData.date.year()}-${
calendarData.date.month() + 1
}-${day}`;
let className = "";
if (
dayjs(fullDay).isSame(period.start) &&
dayjs(fullDay).isSame(period.end)
) {
className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium rounded-full`;
} else if (dayjs(fullDay).isSame(period.start)) {
className = ` ${
BG_COLOR["500"][primaryColor]
} text-white font-medium ${
dayjs(fullDay).isSame(dayHover) && !period.end
? "rounded-full"
: "rounded-l-full"
}`;
} else if (dayjs(fullDay).isSame(period.end)) {
className = ` ${
BG_COLOR["500"][primaryColor]
} text-white font-medium ${
dayjs(fullDay).isSame(dayHover) && !period.start
? "rounded-full"
: "rounded-r-full"
}`;
}
return {
active:
dayjs(fullDay).isSame(period.start) ||
dayjs(fullDay).isSame(period.end),
className: className,
};
},
[calendarData.date, dayHover, period.end, period.start, primaryColor]
);
const hoverClassByDay = useCallback(
(day: number) => {
let className = currentDateClass(day);
const fullDay = `${calendarData.date.year()}-${
calendarData.date.month() + 1
}-${day >= 10 ? day : "0" + day}`;
if (period.start && period.end) {
if (dayjs(fullDay).isBetween(period.start, period.end, "day", "[)")) {
return ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass(
day
)} dark:bg-white/10`;
}
}
if (!dayHover) {
return className;
}
if (
period.start &&
dayjs(fullDay).isBetween(period.start, dayHover, "day", "[)")
) {
className = ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass(
day
)} dark:bg-white/10`;
}
if (
period.end &&
dayjs(fullDay).isBetween(dayHover, period.end, "day", "[)")
) {
className = ` ${BG_COLOR["100"][primaryColor]} ${currentDateClass(
day
)} dark:bg-white/10`;
}
if (dayHover === fullDay) {
const bgColor = BG_COLOR["500"][primaryColor];
className = ` transition-all duration-500 text-white font-medium ${bgColor} ${
period.start ? "rounded-r-full" : "rounded-l-full"
}`;
}
return className;
},
[
calendarData.date,
currentDateClass,
dayHover,
period.end,
period.start,
primaryColor,
]
);
const isDateTooEarly = useCallback(
(day: number, type: "current" | "previous" | "next") => {
if (!minDate) {
return false;
}
const object = {
previous: previousMonth(calendarData.date),
current: calendarData.date,
next: nextMonth(calendarData.date),
};
const newDate = object[type as keyof typeof object];
const formattedDate = newDate.set("date", day);
return dayjs(formattedDate).isSame(dayjs(minDate), "day")
? false
: dayjs(formattedDate).isBefore(dayjs(minDate));
},
[calendarData.date, minDate]
);
const isDateTooLate = useCallback(
(day: number, type: "current" | "previous" | "next") => {
if (!maxDate) {
return false;
}
const object = {
previous: previousMonth(calendarData.date),
current: calendarData.date,
next: nextMonth(calendarData.date),
};
const newDate = object[type as keyof typeof object];
const formattedDate = newDate.set("date", day);
return dayjs(formattedDate).isSame(dayjs(maxDate), "day")
? false
: dayjs(formattedDate).isAfter(dayjs(maxDate));
},
[calendarData.date, maxDate]
);
const isDateDisabled = useCallback(
(day: number, type: "current" | "previous" | "next") => {
if (isDateTooEarly(day, type) || isDateTooLate(day, type)) {
return true;
}
const object = {
previous: previousMonth(calendarData.date),
current: calendarData.date,
next: nextMonth(calendarData.date),
};
const newDate = object[type as keyof typeof object];
const formattedDate = `${newDate.year()}-${newDate.month() + 1}-${
day >= 10 ? day : "0" + day
}`;
if (
!disabledDates ||
(Array.isArray(disabledDates) && !disabledDates.length)
) {
return false;
}
let matchingCount = 0;
disabledDates?.forEach((dateRange) => {
if (
dayjs(formattedDate).isAfter(dateRange.startDate) &&
dayjs(formattedDate).isBefore(dateRange.endDate)
) {
matchingCount++;
}
if (
dayjs(formattedDate).isSame(dateRange.startDate) ||
dayjs(formattedDate).isSame(dateRange.endDate)
) {
matchingCount++;
}
});
return matchingCount > 0;
},
[calendarData.date, isDateTooEarly, isDateTooLate, disabledDates]
);
const buttonClass = useCallback(
(day: number, type: "current" | "next" | "previous") => {
const baseClass =
"flex items-center justify-center w-10 h-10 relative";
if (type === "current") {
return cn(
baseClass,
!activeDateData(day).active
? hoverClassByDay(day)
: activeDateData(day).className,
isDateDisabled(day, type) && "text-gray-400 cursor-not-allowed"
);
}
return cn(
baseClass,
isDateDisabled(day, type) && "cursor-not-allowed",
"text-gray-400"
);
},
[activeDateData, hoverClassByDay, isDateDisabled]
);
const checkIfHoverPeriodContainsDisabledPeriod = useCallback(
(hoverPeriod: Period) => {
if (!Array.isArray(disabledDates)) {
return false;
}
for (let i = 0; i < disabledDates.length; i++) {
if (
dayjs(hoverPeriod.start).isBefore(disabledDates[i].startDate) &&
dayjs(hoverPeriod.end).isAfter(disabledDates[i].endDate)
) {
return true;
}
}
return false;
},
[disabledDates]
);
const getMetaData = useCallback(() => {
return {
previous: previousMonth(calendarData.date),
current: calendarData.date,
next: nextMonth(calendarData.date),
};
}, [calendarData.date]);
const hoverDay = useCallback(
(day: number, type: string) => {
const object = getMetaData();
const newDate = object[type as keyof typeof object];
const newHover = `${newDate.year()}-${newDate.month() + 1}-${
day >= 10 ? day : "0" + day
}`;
if (period.start && !period.end) {
const hoverPeriod = { ...period, end: newHover };
if (dayjs(newHover).isBefore(dayjs(period.start))) {
hoverPeriod.start = newHover;
hoverPeriod.end = period.start;
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
changePeriod({
start: null,
end: period.start,
});
}
}
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
changeDayHover(newHover);
}
}
if (!period.start && period.end) {
const hoverPeriod = { ...period, start: newHover };
if (dayjs(newHover).isAfter(dayjs(period.end))) {
hoverPeriod.start = period.end;
hoverPeriod.end = newHover;
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
changePeriod({
start: period.end,
end: null,
});
}
}
if (!checkIfHoverPeriodContainsDisabledPeriod(hoverPeriod)) {
changeDayHover(newHover);
}
}
},
[
changeDayHover,
changePeriod,
checkIfHoverPeriodContainsDisabledPeriod,
getMetaData,
period,
]
);
const handleClickDay = useCallback(
(day: number, type: "previous" | "current" | "next") => {
function continueClick() {
if (type === "previous") {
onClickPreviousDays(day);
}
if (type === "current") {
onClickDay(day);
}
if (type === "next") {
onClickNextDays(day);
}
}
if (disabledDates?.length) {
const object = getMetaData();
const newDate = object[type as keyof typeof object];
const clickDay = `${newDate.year()}-${newDate.month() + 1}-${
day >= 10 ? day : "0" + day
}`;
if (period.start && !period.end) {
dayjs(clickDay).isSame(dayHover) && continueClick();
} else if (!period.start && period.end) {
dayjs(clickDay).isSame(dayHover) && continueClick();
} else {
continueClick();
}
} else {
continueClick();
}
},
[
dayHover,
disabledDates?.length,
getMetaData,
onClickDay,
onClickNextDays,
onClickPreviousDays,
period.end,
period.start,
]
);
const load_marker = (day: number, type: string) => {
let fullDay = `${calendarData.date.year()}-${
calendarData.date.month() + 1
}-${day >= 10 ? day : "0" + day}`;
if (type === "previous") {
const newDate = previousMonth(calendarData.date);
fullDay = `${newDate.year()}-${newDate.month() + 1}-${
day >= 10 ? day : "0" + day
}`;
}
if (type === "next") {
const newDate = nextMonth(calendarData.date);
fullDay = `${newDate.year()}-${newDate.month() + 1}-${
day >= 10 ? day : "0" + day
}`;
}
const res = new Date(fullDay);
return typeof onIcon === "function" ? onIcon(day, res) : null;
};
return (
<div className="grid grid-cols-7 gap-y-0.5 my-1">
{calendarData.days.previous.map((item, index) => (
<button
type="button"
key={index}
disabled={isDateDisabled(item, "previous")}
className={`${buttonClass(item, "previous")}`}
onClick={() => handleClickDay(item, "previous")}
onMouseOver={() => {
hoverDay(item, "previous");
}}
>
<span className="relative">
{item}
{load_marker(item, "previous")}
</span>
</button>
))}
{calendarData.days.current.map((item, index) => (
<button
type="button"
key={index}
disabled={isDateDisabled(item, "current")}
className={cx(
`${buttonClass(item, "current")}`,
item === 1 && "highlight"
)}
onClick={() => handleClickDay(item, "current")}
onMouseOver={() => {
hoverDay(item, "current");
}}
>
<span className="relative">
{item}
{load_marker(item, "current")}
</span>
</button>
))}
{calendarData.days.next.map((item, index) => (
<button
type="button"
key={index}
disabled={isDateDisabled(item, "next")}
className={`${buttonClass(item, "next")}`}
onClick={() => handleClickDay(item, "next")}
onMouseOver={() => {
hoverDay(item, "next");
}}
>
<span className="relative">
{item}
{load_marker(item, "next")}
</span>
</button>
))}
</div>
);
};
export default Days;

View File

@ -0,0 +1,35 @@
import dayjs from "dayjs";
import React, { useContext } from "react";
import { MONTHS } from "../../constants";
import DatepickerContext from "../../contexts/DatepickerContext";
import { loadLanguageModule } from "../../helpers";
import { RoundedButton } from "../utils";
interface Props {
currentMonth: number;
clickMonth: (month: number) => void;
}
const Months: React.FC<Props> = ({ currentMonth, clickMonth }) => {
const { i18n } = useContext(DatepickerContext);
loadLanguageModule(i18n);
return (
<div className={"w-full grid grid-cols-2 gap-2 mt-2"}>
{MONTHS.map((item) => (
<RoundedButton
key={item}
padding="py-3"
onClick={() => {
clickMonth(item);
}}
active={currentMonth === item}
>
<>{dayjs(`2022-${item}-01`).locale(i18n).format("MMM")}</>
</RoundedButton>
))}
</div>
);
};
export default Months;

View File

@ -0,0 +1,55 @@
import dayjs from "dayjs";
import React, { useContext, useMemo } from "react";
import { DAYS } from "../../constants";
import DatepickerContext from "../../contexts/DatepickerContext";
import { loadLanguageModule, shortString, ucFirst } from "../../helpers";
const Week: React.FC = () => {
const { i18n, startWeekOn } = useContext(DatepickerContext);
loadLanguageModule(i18n);
const startDateModifier = useMemo(() => {
if (startWeekOn) {
switch (startWeekOn) {
case "mon":
return 1;
case "tue":
return 2;
case "wed":
return 3;
case "thu":
return 4;
case "fri":
return 5;
case "sat":
return 6;
case "sun":
return 0;
default:
return 0;
}
}
return 0;
}, [startWeekOn]);
return (
<div className=" grid grid-cols-7 border-b border-gray-300 dark:border-gray-700 py-2">
{DAYS.map((item) => (
<div
key={item}
className="tracking-wide text-gray-500 text-center"
>
{ucFirst(
shortString(
dayjs(`2022-11-${6 + (item + startDateModifier)}`)
.locale(i18n)
.format("ddd")
)
)}
</div>
))}
</div>
);
};
export default Week;

View File

@ -0,0 +1,66 @@
import React, { useContext } from "react";
import { generateArrayNumber } from "../../helpers";
import { RoundedButton } from "../utils";
import DatepickerContext from "../../contexts/DatepickerContext";
interface Props {
year: number;
currentYear: number;
minYear: number | null;
maxYear: number | null;
clickYear: (data: number) => void;
}
const Years: React.FC<Props> = ({
year,
currentYear,
minYear,
maxYear,
clickYear,
}) => {
const { dateLooking } = useContext(DatepickerContext);
let startDate = 0;
let endDate = 0;
switch (dateLooking) {
case "backward":
startDate = year - 11;
endDate = year;
break;
case "middle":
startDate = year - 4;
endDate = year + 7;
break;
case "forward":
default:
startDate = year;
endDate = year + 11;
break;
}
return (
<div className=" w-full grid grid-cols-2 gap-2 mt-2">
{generateArrayNumber(startDate, endDate).map((item, index) => (
<RoundedButton
key={index}
padding="py-3"
onClick={() => {
clickYear(item);
}}
active={currentYear === item}
disabled={
(maxYear !== null && item > maxYear) ||
(minYear !== null && item < minYear)
}
>
<>{item}</>
</RoundedButton>
))}
</div>
);
};
export default Years;

View File

@ -0,0 +1,394 @@
import dayjs from "dayjs";
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { CALENDAR_SIZE, DATE_FORMAT } from "../../constants";
import DatepickerContext from "../../contexts/DatepickerContext";
import {
formatDate,
getDaysInMonth,
getFirstDayInMonth,
getFirstDaysInMonth,
getLastDaysInMonth,
getNumberOfDay,
loadLanguageModule,
nextMonth,
previousMonth,
} from "../../helpers";
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleChevronLeftIcon,
DoubleChevronRightIcon,
RoundedButton,
} from "../utils";
import Days from "./Days";
import Months from "./Months";
import Week from "./Week";
import Years from "./Years";
import { DateType } from "../../types";
interface Props {
date: dayjs.Dayjs;
minDate?: DateType | null;
maxDate?: DateType | null;
onClickPrevious: () => void;
onClickNext: () => void;
changeMonth: (month: number) => void;
changeYear: (year: number) => void;
mode?: "monthly" | "daily";
onMark?: (day: number, date: Date) => any;
}
const Calendar: React.FC<Props> = ({
date,
minDate,
maxDate,
onClickPrevious,
onClickNext,
changeMonth,
changeYear,
onMark,
mode = "daily",
}) => {
// Contexts
const {
period,
changePeriod,
changeDayHover,
showFooter,
changeDatepickerValue,
hideDatepicker,
asSingle,
i18n,
startWeekOn,
input,
} = useContext(DatepickerContext);
loadLanguageModule(i18n);
// States
const [showMonths, setShowMonths] = useState(false);
const [showYears, setShowYears] = useState(false);
const [year, setYear] = useState(date.year());
useEffect(() => {
if (mode === "monthly") {
setShowMonths(true);
hideYears();
}
}, []);
// Functions
const previous = useCallback(() => {
return getLastDaysInMonth(
previousMonth(date),
getNumberOfDay(getFirstDayInMonth(date).ddd, startWeekOn)
);
}, [date, startWeekOn]);
const current = useCallback(() => {
return getDaysInMonth(formatDate(date));
}, [date]);
const next = useCallback(() => {
return getFirstDaysInMonth(
previousMonth(date),
CALENDAR_SIZE - (previous().length + current().length)
);
}, [current, date, previous]);
const hideMonths = useCallback(() => {
showMonths && setShowMonths(false);
}, [showMonths]);
const hideYears = useCallback(() => {
showYears && setShowYears(false);
}, [showYears]);
const clickMonth = useCallback(
(month: number) => {
setTimeout(() => {
changeMonth(month);
if (mode === "daily") {
setShowMonths(!showMonths);
} else {
hideDatepicker();
clickDay(1, month, date.year());
}
}, 250);
},
[changeMonth, showMonths]
);
const clickYear = useCallback(
(year: number) => {
setTimeout(() => {
changeYear(year);
setShowYears(!showYears);
if (mode === "monthly") {
setShowMonths(true);
clickDay(1, date.month() + 1, year);
}
}, 250);
},
[changeYear, showYears]
);
const clickDay = useCallback(
(day: number, month = date.month() + 1, year = date.year()) => {
const fullDay = `${year}-${month}-${day}`;
let newStart;
let newEnd = null;
function chosePeriod(start: string, end: string) {
const ipt = input?.current;
changeDatepickerValue(
{
startDate: dayjs(start).format(DATE_FORMAT),
endDate: dayjs(end).format(DATE_FORMAT),
},
ipt
);
hideDatepicker();
}
if (period.start && period.end) {
if (changeDayHover) {
changeDayHover(null);
}
changePeriod({
start: null,
end: null,
});
}
if ((!period.start && !period.end) || (period.start && period.end)) {
if (!period.start && !period.end) {
changeDayHover(fullDay);
}
newStart = fullDay;
if (asSingle) {
newEnd = fullDay;
chosePeriod(fullDay, fullDay);
}
} else {
if (period.start && !period.end) {
// start not null
// end null
const condition =
dayjs(fullDay).isSame(dayjs(period.start)) ||
dayjs(fullDay).isAfter(dayjs(period.start));
newStart = condition ? period.start : fullDay;
newEnd = condition ? fullDay : period.start;
} else {
// Start null
// End not null
const condition =
dayjs(fullDay).isSame(dayjs(period.end)) ||
dayjs(fullDay).isBefore(dayjs(period.end));
newStart = condition ? fullDay : period.start;
newEnd = condition ? period.end : fullDay;
}
if (!showFooter) {
if (newStart && newEnd) {
chosePeriod(newStart, newEnd);
}
}
}
if (!(newEnd && newStart) || showFooter) {
changePeriod({
start: newStart,
end: newEnd,
});
}
},
[
asSingle,
changeDatepickerValue,
changeDayHover,
changePeriod,
date,
hideDatepicker,
period.end,
period.start,
showFooter,
input,
]
);
const clickPreviousDays = useCallback(
(day: number) => {
const newDate = previousMonth(date);
clickDay(day, newDate.month() + 1, newDate.year());
onClickPrevious();
},
[clickDay, date, onClickPrevious]
);
const clickNextDays = useCallback(
(day: number) => {
const newDate = nextMonth(date);
clickDay(day, newDate.month() + 1, newDate.year());
onClickNext();
},
[clickDay, date, onClickNext]
);
// UseEffects & UseLayoutEffect
useEffect(() => {
setYear(date.year());
}, [date]);
// Variables
const calendarData = useMemo(() => {
return {
date: date,
days: {
previous: previous(),
current: current(),
next: next(),
},
};
}, [current, date, next, previous]);
const minYear = React.useMemo(
() => (minDate && dayjs(minDate).isValid() ? dayjs(minDate).year() : null),
[minDate]
);
const maxYear = React.useMemo(
() => (maxDate && dayjs(maxDate).isValid() ? dayjs(maxDate).year() : null),
[maxDate]
);
return (
<div className="w-full md:w-[296px] md:min-w-[296px]">
<div
className={cx(
"flex items-stretch space-x-1.5 px-2 py-1.5",
css`
border-bottom: 1px solid #d1d5db;
`
)}
>
{!showMonths && !showYears && (
<div className="flex-none flex flex-row items-center">
<RoundedButton roundedFull={true} onClick={onClickPrevious}>
<ChevronLeftIcon className="h-5 w-5" />
</RoundedButton>
</div>
)}
{showYears && (
<div className="flex-none flex flex-row items-center">
<RoundedButton
roundedFull={true}
onClick={() => {
setYear(year - 12);
}}
>
<DoubleChevronLeftIcon className="h-5 w-5" />
</RoundedButton>
</div>
)}
<div className=" flex flex-1 items-stretch space-x-1.5">
<div className="w-1/2 flex items-stretch">
<RoundedButton
onClick={() => {
setShowMonths(!showMonths);
hideYears();
}}
>
<>{calendarData.date.locale(i18n).format("MMM")}</>
</RoundedButton>
</div>
<div className="w-1/2 flex items-stretch">
<RoundedButton
onClick={() => {
setShowYears(!showYears);
hideMonths();
}}
>
<div className="">{calendarData.date.year()}</div>
</RoundedButton>
</div>
</div>
{showYears && (
<div className="flex-none flex flex-row items-center">
<RoundedButton
roundedFull={true}
onClick={() => {
setYear(year + 12);
}}
>
<DoubleChevronRightIcon className="h-5 w-5" />
</RoundedButton>
</div>
)}
{!showMonths && !showYears && (
<div className="flex-none flex flex-row items-center" >
<RoundedButton roundedFull={true} onClick={onClickNext}>
<ChevronRightIcon className="h-5 w-5" />
</RoundedButton>
</div>
)}
</div>
<div className={cx("mt-0.5 min-h-[285px]")}>
{showMonths && (
<Months
currentMonth={calendarData.date.month() + 1}
clickMonth={clickMonth}
/>
)}
{showYears && (
<Years
year={year}
minYear={minYear}
maxYear={maxYear}
currentYear={calendarData.date.year()}
clickYear={clickYear}
/>
)}
{!showMonths && !showYears && (
<>
<Week />
<Days
calendarData={calendarData}
onClickPreviousDays={clickPreviousDays}
onClickDay={clickDay}
onClickNextDays={clickNextDays}
onIcon={(day, date) => {
if(typeof onMark === "function"){
return onMark(day, date)
}
return <></>
if (new Date().getDate() === day)
return (
<div className="absolute inset-y-0 left-0 -translate-y-1/2 -translate-x-1/2">
<div className="w-full h-full flex flex-row items-center justif-center px-0.5">
!
</div>
</div>
);
return <></>
}}
/>
</>
)}
</div>
</div>
);
};
export default Calendar;

View File

@ -0,0 +1,397 @@
import dayjs from "dayjs";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import Calendar from "./Calendar";
import Footer from "./Footer";
import Input from "./Input";
import Shortcuts from "./Shortcuts";
import { COLORS, DATE_FORMAT, DEFAULT_COLOR, LANGUAGE } from "../constants";
import DatepickerContext from "../contexts/DatepickerContext";
import { formatDate, nextMonth, previousMonth } from "../helpers";
import useOnClickOutside from "../hooks";
import { Period, DatepickerType, ColorKeys } from "../types";
import { VerticalDash } from "./utils";
import { useLocal } from "@/lib/utils/use-local";
import { Popover } from "@/lib/components/Popover/Popover";
const Datepicker: React.FC<DatepickerType> = ({
primaryColor = "blue",
value = null,
onChange,
useRange = true,
showFooter = false,
showShortcuts = false,
configs = undefined,
asSingle = false,
placeholder = null,
separator = "~",
startFrom = null,
i18n = LANGUAGE,
disabled = false,
inputClassName = null,
containerClassName = null,
toggleClassName = null,
toggleIcon = undefined,
displayFormat = DATE_FORMAT,
readOnly = false,
minDate = null,
maxDate = null,
dateLooking = "forward",
disabledDates = null,
inputId,
inputName,
startWeekOn = "sun",
classNames = undefined,
popoverDirection = undefined,
mode = "daily",
}) => {
const local = useLocal({ open: false as boolean });
// Ref
const containerRef = useRef<HTMLDivElement | null>(null);
const calendarContainerRef = useRef<HTMLDivElement | null>(null);
const arrowRef = useRef<HTMLDivElement | null>(null);
// State
const [firstDate, setFirstDate] = useState<dayjs.Dayjs>(
startFrom && dayjs(startFrom).isValid() ? dayjs(startFrom) : dayjs()
);
const [secondDate, setSecondDate] = useState<dayjs.Dayjs>(
nextMonth(firstDate)
);
const [period, setPeriod] = useState<Period>({
start: null,
end: null,
});
const [dayHover, setDayHover] = useState<string | null>(null);
const [inputText, setInputText] = useState<string>("");
const [inputRef, setInputRef] = useState(React.createRef<HTMLInputElement>());
// Custom Hooks use
useOnClickOutside(calendarContainerRef, () => {
const container = calendarContainerRef.current;
if (container) {
hideDatepicker();
}
});
// Functions
const hideDatepicker = useCallback(() => {
local.open = false;
local.render();
}, []);
/* Start First */
const firstGotoDate = useCallback(
(date: dayjs.Dayjs) => {
const newDate = dayjs(formatDate(date));
const reformatDate = dayjs(formatDate(secondDate));
if (newDate.isSame(reformatDate) || newDate.isAfter(reformatDate)) {
setSecondDate(nextMonth(date));
}
setFirstDate(date);
},
[secondDate]
);
const previousMonthFirst = useCallback(() => {
setFirstDate(previousMonth(firstDate));
}, [firstDate]);
const nextMonthFirst = useCallback(() => {
firstGotoDate(nextMonth(firstDate));
}, [firstDate, firstGotoDate]);
const changeFirstMonth = useCallback(
(month: number) => {
firstGotoDate(
dayjs(`${firstDate.year()}-${month < 10 ? "0" : ""}${month}-01`)
);
},
[firstDate, firstGotoDate]
);
const changeFirstYear = useCallback(
(year: number) => {
firstGotoDate(dayjs(`${year}-${firstDate.month() + 1}-01`));
},
[firstDate, firstGotoDate]
);
/* End First */
/* Start Second */
const secondGotoDate = useCallback(
(date: dayjs.Dayjs) => {
const newDate = dayjs(formatDate(date, displayFormat));
const reformatDate = dayjs(formatDate(firstDate, displayFormat));
if (newDate.isSame(reformatDate) || newDate.isBefore(reformatDate)) {
setFirstDate(previousMonth(date));
}
setSecondDate(date);
},
[firstDate, displayFormat]
);
const previousMonthSecond = useCallback(() => {
secondGotoDate(previousMonth(secondDate));
}, [secondDate, secondGotoDate]);
const nextMonthSecond = useCallback(() => {
setSecondDate(nextMonth(secondDate));
}, [secondDate]);
const changeSecondMonth = useCallback(
(month: number) => {
secondGotoDate(
dayjs(`${secondDate.year()}-${month < 10 ? "0" : ""}${month}-01`)
);
},
[secondDate, secondGotoDate]
);
const changeSecondYear = useCallback(
(year: number) => {
secondGotoDate(dayjs(`${year}-${secondDate.month() + 1}-01`));
},
[secondDate, secondGotoDate]
);
/* End Second */
// UseEffects & UseLayoutEffect
useEffect(() => {
const container = containerRef.current;
const calendarContainer = calendarContainerRef.current;
const arrow = arrowRef.current;
if (container && calendarContainer && arrow) {
const detail = container.getBoundingClientRect();
const screenCenter = window.innerWidth / 2;
const containerCenter = (detail.right - detail.x) / 2 + detail.x;
if (containerCenter > screenCenter) {
arrow.classList.add("right-0");
arrow.classList.add("mr-3.5");
calendarContainer.classList.add("right-0");
}
}
}, []);
useEffect(() => {
if (value && value.startDate && value.endDate) {
const startDate = dayjs(value.startDate);
const endDate = dayjs(value.endDate);
const validDate = startDate.isValid() && endDate.isValid();
const condition =
validDate && (startDate.isSame(endDate) || startDate.isBefore(endDate));
if (condition) {
setPeriod({
start: formatDate(startDate),
end: formatDate(endDate),
});
setInputText(
`${formatDate(startDate, displayFormat)}${
asSingle
? ""
: ` ${separator} ${formatDate(endDate, displayFormat)}`
}`
);
}
}
if (value && value.startDate === null && value.endDate === null) {
setPeriod({
start: null,
end: null,
});
setInputText("");
}
}, [asSingle, value, displayFormat, separator]);
useEffect(() => {
if (startFrom && dayjs(startFrom).isValid()) {
const startDate = value?.startDate;
const endDate = value?.endDate;
if (startDate && dayjs(startDate).isValid()) {
setFirstDate(dayjs(startDate));
if (!asSingle) {
if (
endDate &&
dayjs(endDate).isValid() &&
dayjs(endDate).startOf("month").isAfter(dayjs(startDate))
) {
setSecondDate(dayjs(endDate));
} else {
setSecondDate(nextMonth(dayjs(startDate)));
}
}
} else {
setFirstDate(dayjs(startFrom));
setSecondDate(nextMonth(dayjs(startFrom)));
}
}
}, [asSingle, startFrom, value]);
// Variables
const safePrimaryColor = useMemo(() => {
if (COLORS.includes(primaryColor)) {
return primaryColor as ColorKeys;
}
return DEFAULT_COLOR;
}, [primaryColor]);
const contextValues = useMemo(() => {
return {
asSingle,
primaryColor: safePrimaryColor,
configs,
calendarContainer: calendarContainerRef,
arrowContainer: arrowRef,
hideDatepicker,
period,
changePeriod: (newPeriod: Period) => setPeriod(newPeriod),
dayHover,
changeDayHover: (newDay: string | null) => setDayHover(newDay),
inputText,
changeInputText: (newText: string) => setInputText(newText),
updateFirstDate: (newDate: dayjs.Dayjs) => firstGotoDate(newDate),
changeDatepickerValue: onChange,
showFooter,
placeholder,
separator,
i18n,
value,
disabled,
inputClassName,
containerClassName,
toggleClassName,
toggleIcon,
readOnly,
displayFormat,
minDate,
maxDate,
dateLooking,
disabledDates,
inputId,
inputName,
startWeekOn,
classNames,
onChange,
input: inputRef,
popoverDirection,
};
}, [
asSingle,
safePrimaryColor,
configs,
hideDatepicker,
period,
dayHover,
inputText,
onChange,
showFooter,
placeholder,
separator,
i18n,
value,
disabled,
inputClassName,
containerClassName,
toggleClassName,
toggleIcon,
readOnly,
displayFormat,
minDate,
maxDate,
dateLooking,
disabledDates,
inputId,
inputName,
startWeekOn,
classNames,
inputRef,
popoverDirection,
firstGotoDate,
]);
const containerClassNameOverload = useMemo(() => {
const defaultContainerClassName = "relative w-full text-gray-700";
return typeof containerClassName === "function"
? containerClassName(defaultContainerClassName)
: typeof containerClassName === "string" && containerClassName !== ""
? containerClassName
: defaultContainerClassName;
}, [containerClassName]);
return (
<DatepickerContext.Provider value={contextValues}>
<Popover
classNameTrigger={ "w-full"}
arrow={false}
className="rounded-md"
onOpenChange={(open: any) => {
if (!disabled) {
local.open = open;
local.render();
}
}}
open={local.open}
content={
<div className={cx("text-md 2xl:text-md")} ref={calendarContainerRef}>
<div className="flex flex-col lg:flex-row py-1">
{showShortcuts && <Shortcuts />}
<div
className={`flex items-stretch flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-1.5 ${
showShortcuts ? "md:pl-2" : "md:pl-1"
} pr-2 lg:pr-1`}
>
<Calendar
date={firstDate}
onClickPrevious={previousMonthFirst}
onClickNext={nextMonthFirst}
changeMonth={changeFirstMonth}
changeYear={changeFirstYear}
mode={mode}
minDate={minDate}
maxDate={maxDate}
/>
{useRange && (
<>
<div className="flex items-center">
<VerticalDash />
</div>
<Calendar
date={secondDate}
onClickPrevious={previousMonthSecond}
onClickNext={nextMonthSecond}
changeMonth={changeSecondMonth}
changeYear={changeSecondYear}
mode={mode}
minDate={minDate}
maxDate={maxDate}
/>
</>
)}
</div>
</div>
{showFooter && <Footer />}
</div>
}
>
<div className={containerClassNameOverload}>
<Input setContextRef={setInputRef} />
</div>
</Popover>
</DatepickerContext.Provider>
);
};
export default Datepicker;

View File

@ -0,0 +1,55 @@
import dayjs from "dayjs";
import React, { useCallback, useContext } from "react";
import { DATE_FORMAT } from "../constants";
import DatepickerContext from "../contexts/DatepickerContext";
import { PrimaryButton, SecondaryButton } from "./utils";
const Footer: React.FC = () => {
// Contexts
const { hideDatepicker, period, changeDatepickerValue, configs, classNames } =
useContext(DatepickerContext);
// Functions
const getClassName = useCallback(() => {
if (
typeof classNames !== "undefined" &&
typeof classNames?.footer === "function"
) {
return classNames.footer();
}
return " flex items-center justify-end pb-2.5 pt-3 border-t border-gray-300 dark:border-gray-700";
}, [classNames]);
return (
<div className={getClassName()}>
<div className="w-full md:w-auto flex items-center justify-center space-x-3">
<SecondaryButton
onClick={() => {
hideDatepicker();
}}
>
<>{configs?.footer?.cancel ? configs.footer.cancel : "Cancel"}</>
</SecondaryButton>
<PrimaryButton
onClick={() => {
if (period.start && period.end) {
changeDatepickerValue({
startDate: dayjs(period.start).format(DATE_FORMAT),
endDate: dayjs(period.end).format(DATE_FORMAT),
});
hideDatepicker();
}
}}
disabled={!(period.start && period.end)}
>
<>{configs?.footer?.apply ? configs.footer.apply : "Apply"}</>
</PrimaryButton>
</div>
</div>
);
};
export default Footer;

View File

@ -0,0 +1,327 @@
import dayjs from "dayjs";
import React, { useCallback, useContext, useEffect, useRef } from "react";
import { BORDER_COLOR, DATE_FORMAT, RING_COLOR } from "../constants";
import DatepickerContext from "../contexts/DatepickerContext";
import { dateIsValid, parseFormattedDate } from "../helpers";
import ToggleButton from "./ToggleButton";
type Props = {
setContextRef?: (ref: React.RefObject<HTMLInputElement>) => void;
};
const Input: React.FC<Props> = (e: Props) => {
// Context
const {
primaryColor,
period,
dayHover,
changeDayHover,
calendarContainer,
arrowContainer,
inputText,
changeInputText,
hideDatepicker,
changeDatepickerValue,
asSingle,
placeholder,
separator,
disabled,
inputClassName,
toggleClassName,
toggleIcon,
readOnly,
displayFormat,
inputId,
inputName,
classNames,
popoverDirection,
} = useContext(DatepickerContext);
// UseRefs
const buttonRef = useRef<HTMLButtonElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
// Functions
const getClassName = useCallback(() => {
const input = inputRef.current;
if (
input &&
typeof classNames !== "undefined" &&
typeof classNames?.input === "function"
) {
return classNames.input(input);
}
const border =
BORDER_COLOR.focus[primaryColor as keyof typeof BORDER_COLOR.focus];
const ring =
RING_COLOR["second-focus"][
primaryColor as keyof (typeof RING_COLOR)["second-focus"]
];
const defaultInputClassName = `relative flex h-9 w-full rounded-md border border-gray-200 border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm ${border} ${ring}`;
return typeof inputClassName === "function"
? inputClassName(defaultInputClassName)
: typeof inputClassName === "string" && inputClassName !== ""
? inputClassName
: defaultInputClassName;
}, [inputRef, classNames, primaryColor, inputClassName]);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const inputValue = e.target.value;
const dates = [];
if (asSingle) {
const date = parseFormattedDate(inputValue, displayFormat);
if (dateIsValid(date.toDate())) {
dates.push(date.format(DATE_FORMAT));
}
} else {
const parsed = inputValue.split(separator);
let startDate = null;
let endDate = null;
if (parsed.length === 2) {
startDate = parseFormattedDate(parsed[0], displayFormat);
endDate = parseFormattedDate(parsed[1], displayFormat);
} else {
const middle = Math.floor(inputValue.length / 2);
startDate = parseFormattedDate(
inputValue.slice(0, middle),
displayFormat
);
endDate = parseFormattedDate(inputValue.slice(middle), displayFormat);
}
if (
dateIsValid(startDate.toDate()) &&
dateIsValid(endDate.toDate()) &&
startDate.isBefore(endDate)
) {
dates.push(startDate.format(DATE_FORMAT));
dates.push(endDate.format(DATE_FORMAT));
}
}
if (dates[0]) {
changeDatepickerValue(
{
startDate: dates[0],
endDate: dates[1] || dates[0],
},
e.target
);
if (dates[1])
changeDayHover(dayjs(dates[1]).add(-1, "day").format(DATE_FORMAT));
else changeDayHover(dates[0]);
}
changeInputText(e.target.value);
},
[
asSingle,
displayFormat,
separator,
changeDatepickerValue,
changeDayHover,
changeInputText,
]
);
const handleInputKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
const input = inputRef.current;
if (input) {
input.blur();
}
hideDatepicker();
}
},
[hideDatepicker]
);
const renderToggleIcon = useCallback(
(isEmpty: boolean) => {
return typeof toggleIcon === "undefined" ? (
<ToggleButton isEmpty={isEmpty} />
) : (
toggleIcon(isEmpty)
);
},
[toggleIcon]
);
const getToggleClassName = useCallback(() => {
const button = buttonRef.current;
if (
button &&
typeof classNames !== "undefined" &&
typeof classNames?.toggleButton === "function"
) {
return classNames.toggleButton(button);
}
const defaultToggleClassName =
"absolute right-0 top-0 h-full px-3 text-gray-400 focus:outline-none disabled:opacity-40 disabled:cursor-not-allowed";
return typeof toggleClassName === "function"
? toggleClassName(defaultToggleClassName)
: typeof toggleClassName === "string" && toggleClassName !== ""
? toggleClassName
: defaultToggleClassName;
}, [toggleClassName, buttonRef, classNames]);
// UseEffects && UseLayoutEffect
useEffect(() => {
if (inputRef && e.setContextRef && typeof e.setContextRef === "function") {
e.setContextRef(inputRef);
}
}, [e, inputRef]);
useEffect(() => {
const button = buttonRef?.current;
function focusInput(e: Event) {
e.stopPropagation();
const input = inputRef.current;
if (input) {
input.focus();
if (inputText) {
changeInputText("");
if (dayHover) {
changeDayHover(null);
}
if (period.start && period.end) {
changeDatepickerValue(
{
startDate: null,
endDate: null,
},
input
);
}
}
}
}
if (button) {
button.addEventListener("click", focusInput);
}
return () => {
if (button) {
button.removeEventListener("click", focusInput);
}
};
}, [
changeDatepickerValue,
changeDayHover,
changeInputText,
dayHover,
inputText,
period.end,
period.start,
inputRef,
]);
useEffect(() => {
const div = calendarContainer?.current;
const input = inputRef.current;
const arrow = arrowContainer?.current;
function showCalendarContainer() {
if (arrow && div && div.classList.contains("hidden")) {
div.classList.remove("hidden");
div.classList.add("block");
// window.innerWidth === 767
const popoverOnUp = popoverDirection == "up";
const popoverOnDown = popoverDirection === "down";
if (
popoverOnUp ||
(window.innerWidth > 767 &&
window.screen.height - 100 < div.getBoundingClientRect().bottom &&
!popoverOnDown)
) {
div.classList.add("bottom-full");
div.classList.add("mb-2.5");
div.classList.remove("mt-2.5");
arrow.classList.add("-bottom-2");
arrow.classList.add("border-r");
arrow.classList.add("border-b");
arrow.classList.remove("border-l");
arrow.classList.remove("border-t");
}
setTimeout(() => {
div.classList.remove("translate-y-4");
div.classList.remove("opacity-0");
div.classList.add("translate-y-0");
div.classList.add("opacity-1");
}, 1);
}
}
if (div && input) {
input.addEventListener("focus", showCalendarContainer);
}
return () => {
if (input) {
input.removeEventListener("focus", showCalendarContainer);
}
};
}, [calendarContainer, arrowContainer, popoverDirection]);
return (
<>
{disabled ? (
<div className={"flex h-9 w-full rounded-md border border-gray-200 border-input bg-gray-100 items-center px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"}>{inputText ? inputText : "-"}</div>
) : (
<input
ref={inputRef}
type="text"
className={getClassName()}
readOnly={readOnly}
placeholder={
placeholder
? placeholder
: `${displayFormat}${
asSingle ? "" : ` ${separator} ${displayFormat}`
}`
}
value={inputText}
id={inputId}
name={inputName}
autoComplete="off"
role="presentation"
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
/>
)}
{!disabled && (
<button
type="button"
ref={buttonRef}
disabled={disabled}
className={getToggleClassName()}
>
{renderToggleIcon(inputText == null || !inputText?.length)}
</button>
)}
</>
);
};
export default Input;

View File

@ -0,0 +1,179 @@
import dayjs from "dayjs";
import React, { useCallback, useContext, useMemo } from "react";
import { DATE_FORMAT, TEXT_COLOR } from "../constants";
import DEFAULT_SHORTCUTS from "../constants/shortcuts";
import DatepickerContext from "../contexts/DatepickerContext";
import { Period, ShortcutsItem } from "../types";
interface ItemTemplateProps {
children: any;
key: number;
item: ShortcutsItem | ShortcutsItem[];
}
// eslint-disable-next-line react/display-name
const ItemTemplate = React.memo((props: ItemTemplateProps) => {
const {
primaryColor,
period,
changePeriod,
updateFirstDate,
dayHover,
changeDayHover,
hideDatepicker,
changeDatepickerValue,
} = useContext(DatepickerContext);
// Functions
const getClassName: () => string = useCallback(() => {
const textColor =
TEXT_COLOR["600"][primaryColor as keyof (typeof TEXT_COLOR)["600"]];
const textColorHover =
TEXT_COLOR.hover[primaryColor as keyof typeof TEXT_COLOR.hover];
return `whitespace-nowrap w-1/2 md:w-1/3 lg:w-auto transition-all duration-300 hover:bg-gray-100 dark:hover:bg-white/10 p-2 rounded cursor-pointer ${textColor} ${textColorHover}`;
}, [primaryColor]);
const chosePeriod = useCallback(
(item: Period) => {
if (dayHover) {
changeDayHover(null);
}
if (period.start || period.end) {
changePeriod({
start: null,
end: null,
});
}
changePeriod(item);
changeDatepickerValue({
startDate: item.start,
endDate: item.end,
});
updateFirstDate(dayjs(item.start));
hideDatepicker();
},
[
changeDatepickerValue,
changeDayHover,
changePeriod,
dayHover,
hideDatepicker,
period.end,
period.start,
updateFirstDate,
]
);
const children = props?.children;
return (
<li
className={getClassName()}
onClick={() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
chosePeriod(props?.item.period);
}}
>
{children}
</li>
);
});
const Shortcuts: React.FC = () => {
// Contexts
const { configs } = useContext(DatepickerContext);
const callPastFunction = useCallback((data: unknown, numberValue: number) => {
return typeof data === "function" ? data(numberValue) : null;
}, []);
const shortcutOptions = useMemo<
[string, ShortcutsItem | ShortcutsItem[]][]
>(() => {
if (!configs?.shortcuts) {
return Object.entries(DEFAULT_SHORTCUTS);
}
return Object.entries(configs.shortcuts).flatMap(([key, customConfig]) => {
if (Object.prototype.hasOwnProperty.call(DEFAULT_SHORTCUTS, key)) {
return [[key, DEFAULT_SHORTCUTS[key]]];
}
const { text, period } = customConfig as {
text: string;
period: { start: string; end: string };
};
if (!text || !period) {
return [];
}
const start = dayjs(period.start);
const end = dayjs(period.end);
if (
start.isValid() &&
end.isValid() &&
(start.isBefore(end) || start.isSame(end))
) {
return [
[
text,
{
text,
period: {
start: start.format(DATE_FORMAT),
end: end.format(DATE_FORMAT),
},
},
],
];
}
return [];
});
}, [configs]);
const printItemText = useCallback((item: ShortcutsItem) => {
return item?.text ?? null;
}, []);
return shortcutOptions?.length ? (
<div className="md:border-b mb-3 lg:mb-0 lg:border-r lg:border-b-0 border-gray-300 dark:border-gray-700 pr-1">
<ul className="w-full tracking-wide flex flex-wrap lg:flex-col pb-1 lg:pb-0">
{shortcutOptions.map(([key, item], index: number) =>
Array.isArray(item) ? (
item.map((item, index) => (
<ItemTemplate key={index} item={item}>
<>
{key === "past" &&
configs?.shortcuts &&
key in configs.shortcuts &&
item.daysNumber
? callPastFunction(
configs.shortcuts[key as "past"],
item.daysNumber
)
: item.text}
</>
</ItemTemplate>
))
) : (
<ItemTemplate key={index} item={item}>
{configs?.shortcuts && key in configs.shortcuts
? typeof configs.shortcuts[
key as keyof typeof configs.shortcuts
] === "object"
? printItemText(item)
: configs.shortcuts[key as keyof typeof configs.shortcuts]
: printItemText(item)}
</ItemTemplate>
)
)}
</ul>
</div>
) : null;
};
export default Shortcuts;

View File

@ -0,0 +1,19 @@
import React from "react";
import { CloseIcon, DateIcon } from "./utils";
interface ToggleButtonProps {
isEmpty: boolean;
}
const ToggleButton: React.FC<ToggleButtonProps> = (
e: ToggleButtonProps
): JSX.Element => {
return e.isEmpty ? (
<DateIcon className="h-5 w-5" />
) : (
<CloseIcon className="h-5 w-5" />
);
};
export default ToggleButton;

View File

@ -0,0 +1,254 @@
import React, { useCallback, useContext } from "react";
import { BG_COLOR, BORDER_COLOR, BUTTON_COLOR, RING_COLOR } from "../constants";
import DatepickerContext from "../contexts/DatepickerContext";
interface IconProps {
className: string;
}
interface Button {
children: JSX.Element | JSX.Element[];
onClick: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
roundedFull?: boolean;
padding?: string;
active?: boolean;
}
export const DateIcon: React.FC<IconProps> = ({ className = "w-6 h-6" }) => {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5m-9-6h.008v.008H12v-.008zM12 15h.008v.008H12V15zm0 2.25h.008v.008H12v-.008zM9.75 15h.008v.008H9.75V15zm0 2.25h.008v.008H9.75v-.008zM7.5 15h.008v.008H7.5V15zm0 2.25h.008v.008H7.5v-.008zm6.75-4.5h.008v.008h-.008v-.008zm0 2.25h.008v.008h-.008V15zm0 2.25h.008v.008h-.008v-.008zm2.25-4.5h.008v.008H16.5v-.008zm0 2.25h.008v.008H16.5V15z"
/>
</svg>
);
};
export const CloseIcon: React.FC<IconProps> = ({ className = "w-6 h-6" }) => {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
);
};
export const ChevronLeftIcon: React.FC<IconProps> = ({
className = "w-6 h-6",
}) => {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 19.5L8.25 12l7.5-7.5"
/>
</svg>
);
};
export const DoubleChevronLeftIcon: React.FC<IconProps> = ({
className = "w-6 h-6",
}) => {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18.75 19.5l-7.5-7.5 7.5-7.5m-6 15L5.25 12l7.5-7.5"
/>
</svg>
);
};
export const ChevronRightIcon: React.FC<IconProps> = ({
className = "w-6 h-6",
}) => {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5"
/>
</svg>
);
};
export const DoubleChevronRightIcon: React.FC<IconProps> = ({
className = "w-6 h-6",
}) => {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.25 4.5l7.5 7.5-7.5 7.5m-6-15l7.5 7.5-7.5 7.5"
/>
</svg>
);
};
export const SecondaryButton: React.FC<Button> = ({
children,
onClick,
disabled = false,
}) => {
// Contexts
const { primaryColor } = useContext(DatepickerContext);
// Functions
const getClassName: () => string = useCallback(() => {
const ringColor =
RING_COLOR.focus[primaryColor as keyof typeof RING_COLOR.focus];
return ` w-full transition-all duration-300 bg-gray-50 font-medium border border-gray-300 px-4 py-2.5 text-md rounded-md focus:ring-2 focus:ring-offset-2 hover:bg-gray-50 ${ringColor}`;
}, [primaryColor]);
return (
<button
type="button"
className={getClassName()}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
export const PrimaryButton: React.FC<Button> = ({
children,
onClick,
disabled = false,
}) => {
// Contexts
const { primaryColor } = useContext(DatepickerContext);
const bgColor =
BG_COLOR["500"][primaryColor as keyof (typeof BG_COLOR)["500"]];
const borderColor =
BORDER_COLOR["500"][primaryColor as keyof (typeof BORDER_COLOR)["500"]];
const bgColorHover =
BG_COLOR.hover[primaryColor as keyof typeof BG_COLOR.hover];
const ringColor =
RING_COLOR.focus[primaryColor as keyof typeof RING_COLOR.focus];
// Functions
const getClassName = useCallback(() => {
return ` w-full transition-all duration-300 ${bgColor} ${borderColor} text-white font-medium border px-4 py-2 text-md rounded-md focus:ring-2 focus:ring-offset-2 ${bgColorHover} ${ringColor} ${
disabled ? " cursor-no-drop" : ""
}`;
}, [bgColor, bgColorHover, borderColor, disabled, ringColor]);
return (
<button
type="button"
className={getClassName()}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
export const RoundedButton: React.FC<Button> = ({
children,
onClick,
disabled,
roundedFull = false,
padding = "py-[0.55rem]",
active = false,
}) => {
// Contexts
const { primaryColor } = useContext(DatepickerContext);
// Functions
const getClassName = useCallback(() => {
const darkClass =
"";
const activeClass = active
? "font-semibold bg-gray-50 "
: "";
const defaultClass = !roundedFull
? `w-full tracking-wide ${darkClass} ${activeClass} transition-all duration-300 ${padding} uppercase hover:bg-gray-100 rounded-md focus:ring-1`
: `${darkClass} ${activeClass} transition-all duration-300 hover:bg-gray-100 rounded-full p-[0.45rem] focus:ring-1`;
const buttonFocusColor =
BUTTON_COLOR.focus[primaryColor as keyof typeof BUTTON_COLOR.focus];
const disabledClass = disabled ? "line-through" : "";
return `${defaultClass} ${buttonFocusColor} ${disabledClass}`;
}, [disabled, padding, primaryColor, roundedFull, active]);
return (
<button
type="button"
className={getClassName()}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
export const VerticalDash = () => {
// Contexts
const { primaryColor } = useContext(DatepickerContext);
const bgColor =
BG_COLOR["500"][primaryColor as keyof (typeof BG_COLOR)["500"]];
return (
<div
className={`bg-blue-500 h-7 w-1 rounded-full hidden md:block ${bgColor}`}
/>
);
};

View File

@ -0,0 +1,285 @@
import { ColorKeys, Colors } from "../types";
export const COLORS = [
"blue",
"orange",
"yellow",
"red",
"purple",
"amber",
"lime",
"green",
"emerald",
"teal",
"cyan",
"sky",
"indigo",
"violet",
"purple",
"fuchsia",
"pink",
"rose",
] as const;
export const DEFAULT_COLOR: ColorKeys = "blue";
export const LANGUAGE = "en";
export const DATE_FORMAT = "YYYY-MM-DD";
export const START_WEEK = "sun";
export const DATE_LOOKING_OPTIONS = ["forward", "backward", "middle"];
export const DAYS = [0, 1, 2, 3, 4, 5, 6];
export const MONTHS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
export const CALENDAR_SIZE = 42;
// Beware, these maps of colors cannot be replaced with functions using string interpolation such as `bg-${color}-100`
// as described in Tailwind documentation https://tailwindcss.com/docs/content-configuration#dynamiclass-names
export const BG_COLOR: Colors = {
100: {
blue: "bg-blue-100",
orange: "bg-orange-100",
yellow: "bg-yellow-100",
red: "bg-red-100",
purple: "bg-purple-100",
amber: "bg-amber-100",
lime: "bg-lime-100",
green: "bg-green-100",
emerald: "bg-emerald-100",
teal: "bg-teal-100",
cyan: "bg-cyan-100",
sky: "bg-sky-100",
indigo: "bg-indigo-100",
violet: "bg-violet-100",
fuchsia: "bg-fuchsia-100",
pink: "bg-pink-100",
rose: "bg-rose-100",
},
200: {
blue: "bg-blue-200",
orange: "bg-orange-200",
yellow: "bg-yellow-200",
red: "bg-red-200",
purple: "bg-purple-200",
amber: "bg-amber-200",
lime: "bg-lime-200",
green: "bg-green-200",
emerald: "bg-emerald-200",
teal: "bg-teal-200",
cyan: "bg-cyan-200",
sky: "bg-sky-200",
indigo: "bg-indigo-200",
violet: "bg-violet-200",
fuchsia: "bg-fuchsia-200",
pink: "bg-pink-200",
rose: "bg-rose-200",
},
500: {
blue: "bg-blue-500",
orange: "bg-orange-500",
yellow: "bg-yellow-500",
red: "bg-red-500",
purple: "bg-purple-500",
amber: "bg-amber-500",
lime: "bg-lime-500",
green: "bg-green-500",
emerald: "bg-emerald-500",
teal: "bg-teal-500",
cyan: "bg-cyan-500",
sky: "bg-sky-500",
indigo: "bg-indigo-500",
violet: "bg-violet-500",
fuchsia: "bg-fuchsia-500",
pink: "bg-pink-500",
rose: "bg-rose-500",
},
hover: {
blue: "hover:bg-blue-600",
orange: "hover:bg-orange-600",
yellow: "hover:bg-yellow-600",
red: "hover:bg-red-600",
purple: "hover:bg-purple-600",
amber: "hover:bg-amber-600",
lime: "hover:bg-lime-600",
green: "hover:bg-green-600",
emerald: "hover:bg-emerald-600",
teal: "hover:bg-teal-600",
cyan: "hover:bg-cyan-600",
sky: "hover:bg-sky-600",
indigo: "hover:bg-indigo-600",
violet: "hover:bg-violet-600",
fuchsia: "hover:bg-fuchsia-600",
pink: "hover:bg-pink-600",
rose: "hover:bg-rose-600",
},
};
export const TEXT_COLOR: Colors = {
500: {
blue: "text-blue-500",
orange: "text-orange-500",
yellow: "text-yellow-500",
red: "text-red-500",
purple: "text-purple-500",
amber: "text-amber-500",
lime: "text-lime-500",
green: "text-green-500",
emerald: "text-emerald-500",
teal: "text-teal-500",
cyan: "text-cyan-500",
sky: "text-sky-500",
indigo: "text-indigo-500",
violet: "text-violet-500",
fuchsia: "text-fuchsia-500",
pink: "text-pink-500",
rose: "text-rose-500",
},
600: {
blue: "text-blue-600",
orange: "text-orange-600 ",
yellow: "text-yellow-600 ",
red: "text-red-600 ",
purple: "text-purple-600 ",
amber: "text-amber-600 ",
lime: "text-lime-600 ",
green: "text-green-600 ",
emerald:
"text-emerald-600 ",
teal: "text-teal-600 ",
cyan: "text-cyan-600 ",
sky: "text-sky-600 ",
indigo: "text-indigo-600 ",
violet: "text-violet-600 ",
fuchsia:
"text-fuchsia-600 ",
pink: "text-pink-600 ",
rose: "text-rose-600 ",
},
hover: {
blue: "hover:text-blue-700",
orange: "hover:text-orange-700",
yellow: "hover:text-yellow-700",
red: "hover:text-red-700",
purple: "hover:text-purple-700",
amber: "hover:text-amber-700",
lime: "hover:text-lime-700",
green: "hover:text-green-700",
emerald: "hover:text-emerald-700",
teal: "hover:text-teal-700",
cyan: "hover:text-cyan-700",
sky: "hover:text-sky-700",
indigo: "hover:text-indigo-700",
violet: "hover:text-violet-700",
fuchsia: "hover:text-fuchsia-700",
pink: "hover:text-pink-700",
rose: "hover:text-rose-700",
},
};
export const BORDER_COLOR: Colors = {
500: {
blue: "border-blue-500",
orange: "border-orange-500",
yellow: "border-yellow-500",
red: "border-red-500",
purple: "border-purple-500",
amber: "border-amber-500",
lime: "border-lime-500",
green: "border-green-500",
emerald: "border-emerald-500",
teal: "border-teal-500",
cyan: "border-cyan-500",
sky: "border-sky-500",
indigo: "border-indigo-500",
violet: "border-violet-500",
fuchsia: "border-fuchsia-500",
pink: "border-pink-500",
rose: "border-rose-500",
},
focus: {
blue: "focus:border-blue-500",
orange: "focus:border-orange-500",
yellow: "focus:border-yellow-500",
red: "focus:border-red-500",
purple: "focus:border-purple-500",
amber: "focus:border-amber-500",
lime: "focus:border-lime-500",
green: "focus:border-green-500",
emerald: "focus:border-emerald-500",
teal: "focus:border-teal-500",
cyan: "focus:border-cyan-500",
sky: "focus:border-sky-500",
indigo: "focus:border-indigo-500",
violet: "focus:border-violet-500",
fuchsia: "focus:border-fuchsia-500",
pink: "focus:border-pink-500",
rose: "focus:border-rose-500",
},
};
export const RING_COLOR: Colors = {
focus: {
blue: "focus:ring-blue-500",
orange: "focus:ring-orange-500",
yellow: "focus:ring-yellow-500",
red: "focus:ring-red-500",
purple: "focus:ring-purple-500",
amber: "focus:ring-amber-500",
lime: "focus:ring-lime-500",
green: "focus:ring-green-500",
emerald: "focus:ring-emerald-500",
teal: "focus:ring-teal-500",
cyan: "focus:ring-cyan-500",
sky: "focus:ring-sky-500",
indigo: "focus:ring-indigo-500",
violet: "focus:ring-violet-500",
fuchsia: "focus:ring-fuchsia-500",
pink: "focus:ring-pink-500",
rose: "focus:ring-rose-500",
},
"second-focus": {
blue: "focus:ring-blue-500/20",
orange: "focus:ring-orange-500/20",
yellow: "focus:ring-yellow-500/20",
red: "focus:ring-red-500/20",
purple: "focus:ring-purple-500/20",
amber: "focus:ring-amber-500/20",
lime: "focus:ring-lime-500/20",
green: "focus:ring-green-500/20",
emerald: "focus:ring-emerald-500/20",
teal: "focus:ring-teal-500/20",
cyan: "focus:ring-cyan-500/20",
sky: "focus:ring-sky-500/20",
indigo: "focus:ring-indigo-500/20",
violet: "focus:ring-violet-500/20",
fuchsia: "focus:ring-fuchsia-500/20",
pink: "focus:ring-pink-500/20",
rose: "focus:ring-rose-500/20",
},
};
export const BUTTON_COLOR: Colors = {
focus: {
blue: "focus:ring-blue-500/50 focus:bg-blue-100/50",
orange: "focus:ring-orange-500/50 focus:bg-orange-100/50",
yellow: "focus:ring-yellow-500/50 focus:bg-yellow-100/50",
red: "focus:ring-red-500/50 focus:bg-red-100/50",
purple: "focus:ring-purple-500/50 focus:bg-purple-100/50",
amber: "focus:ring-amber-500/50 focus:bg-amber-100/50",
lime: "focus:ring-lime-500/50 focus:bg-lime-100/50",
green: "focus:ring-green-500/50 focus:bg-green-100/50",
emerald: "focus:ring-emerald-500/50 focus:bg-emerald-100/50",
teal: "focus:ring-teal-500/50 focus:bg-teal-100/50",
cyan: "focus:ring-cyan-500/50 focus:bg-cyan-100/50",
sky: "focus:ring-sky-500/50 focus:bg-sky-100/50",
indigo: "focus:ring-indigo-500/50 focus:bg-indigo-100/50",
violet: "focus:ring-violet-500/50 focus:bg-violet-100/50",
fuchsia: "focus:ring-fuchsia-500/50 focus:bg-fuchsia-100/50",
pink: "focus:ring-pink-500/50 focus:bg-pink-100/50",
rose: "focus:ring-rose-500/50 focus:bg-rose-100/50",
},
};

View File

@ -0,0 +1,57 @@
import dayjs from "dayjs";
import { formatDate, previousMonth } from "../helpers";
import { ShortcutsItem } from "../types";
const DEFAULT_SHORTCUTS: {
[key in string]: ShortcutsItem | ShortcutsItem[];
} = {
today: {
text: "Today",
period: {
start: formatDate(dayjs()),
end: formatDate(dayjs()),
},
},
yesterday: {
text: "Yesterday",
period: {
start: formatDate(dayjs().subtract(1, "d")),
end: formatDate(dayjs().subtract(1, "d")),
},
},
past: [
{
daysNumber: 7,
text: "Last 7 days",
period: {
start: formatDate(dayjs().subtract(7, "d")),
end: formatDate(dayjs()),
},
},
{
daysNumber: 30,
text: "Last 30 days",
period: {
start: formatDate(dayjs().subtract(30, "d")),
end: formatDate(dayjs()),
},
},
],
currentMonth: {
text: "This month",
period: {
start: formatDate(dayjs().startOf("month")),
end: formatDate(dayjs().endOf("month")),
},
},
pastMonth: {
text: "Last month",
period: {
start: formatDate(previousMonth(dayjs()).startOf("month")),
end: formatDate(previousMonth(dayjs()).endOf("month")),
},
},
};
export default DEFAULT_SHORTCUTS;

View File

@ -0,0 +1,104 @@
import dayjs from "dayjs";
import React, { createContext } from "react";
import { DATE_FORMAT, LANGUAGE, START_WEEK } from "../constants";
import {
Configs,
Period,
DateValueType,
DateType,
DateRangeType,
ClassNamesTypeProp,
PopoverDirectionType,
ColorKeys,
} from "../types";
interface DatepickerStore {
input?: React.RefObject<HTMLInputElement>;
asSingle?: boolean;
primaryColor: ColorKeys;
configs?: Configs;
calendarContainer: React.RefObject<HTMLDivElement> | null;
arrowContainer: React.RefObject<HTMLDivElement> | null;
hideDatepicker: () => void;
period: Period;
changePeriod: (period: Period) => void;
dayHover: string | null;
changeDayHover: (day: string | null) => void;
inputText: string;
changeInputText: (text: string) => void;
updateFirstDate: (date: dayjs.Dayjs) => void;
changeDatepickerValue: (
value: DateValueType,
e?: HTMLInputElement | null | undefined
) => void;
showFooter?: boolean;
placeholder?: string | null;
separator: string;
i18n: string;
value: DateValueType;
disabled?: boolean;
inputClassName?: ((className: string) => string) | string | null;
containerClassName?: ((className: string) => string) | string | null;
toggleClassName?: ((className: string) => string) | string | null;
toggleIcon?: (open: boolean) => React.ReactNode;
readOnly?: boolean;
startWeekOn?: string | null;
displayFormat: string;
minDate?: DateType | null;
maxDate?: DateType | null;
dateLooking?: "forward" | "backward" | "middle";
disabledDates?: DateRangeType[] | null;
inputId?: string;
inputName?: string;
classNames?: ClassNamesTypeProp;
popoverDirection?: PopoverDirectionType;
}
const DatepickerContext = createContext<DatepickerStore>({
input: undefined,
primaryColor: "blue",
configs: undefined,
calendarContainer: null,
arrowContainer: null,
period: { start: null, end: null },
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
changePeriod: (period) => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function
hideDatepicker: () => {},
dayHover: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
changeDayHover: (day: string | null) => {},
inputText: "",
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
changeInputText: (text) => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
updateFirstDate: (date) => {},
// eslint-disable-next-line @typescript-eslint/no-empty-function,@typescript-eslint/no-unused-vars
changeDatepickerValue: (
value: DateValueType,
e: HTMLInputElement | null | undefined
) => {},
showFooter: false,
value: null,
i18n: LANGUAGE,
disabled: false,
inputClassName: "",
containerClassName: "",
toggleClassName: "",
readOnly: false,
displayFormat: DATE_FORMAT,
minDate: null,
maxDate: null,
dateLooking: "forward",
disabledDates: null,
inputId: undefined,
inputName: undefined,
startWeekOn: START_WEEK,
toggleIcon: undefined,
classNames: undefined,
popoverDirection: undefined,
separator: "~",
});
export default DatepickerContext;

View File

@ -0,0 +1,632 @@
import dayjs from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
import weekday from "dayjs/plugin/weekday";
dayjs.extend(weekday);
dayjs.extend(customParseFormat);
import { DATE_FORMAT, LANGUAGE } from "../constants";
export function classNames(...classes: (false | null | undefined | string)[]) {
return classes.filter(Boolean).join(" ");
}
export function getTextColorByPrimaryColor(color: string) {
switch (color) {
case "blue":
return "text-blue-500";
case "orange":
return "text-orange-500";
case "yellow":
return "text-yellow-500";
case "red":
return "text-red-500";
case "purple":
return "text-purple-500";
case "amber":
return "text-amber-500";
case "lime":
return "text-lime-500";
case "green":
return "text-green-500";
case "emerald":
return "text-emerald-500";
case "teal":
return "text-teal-500";
case "cyan":
return "text-cyan-500";
case "sky":
return "text-sky-500";
case "indigo":
return "text-indigo-500";
case "violet":
return "text-violet-500";
case "fuchsia":
return "text-fuchsia-500";
case "pink":
return "text-pink-500";
case "rose":
return "text-rose-500";
default:
return "";
}
}
export function generateArrayNumber(start = 0, end = 0) {
const array = [];
for (let i = start; i <= end; i++) {
array.push(i);
}
return array;
}
export function shortString(value: string, limit = 3) {
return value.slice(0, limit);
}
export function ucFirst(value: string) {
return `${value[0].toUpperCase()}${value.slice(1, value.length)}`;
}
export function formatDate(date: dayjs.Dayjs, format = DATE_FORMAT) {
return date.format(format);
}
export function parseFormattedDate(date: string, format = DATE_FORMAT) {
return dayjs(date, format);
}
export function getFirstDayInMonth(date: string | dayjs.Dayjs) {
return {
ddd: formatDate(dayjs(date).startOf("month"), "ddd"),
basic: formatDate(dayjs(date).startOf("month")),
object: dayjs(date).startOf("month"),
};
}
export function getLastDayInMonth(date: string) {
return {
ddd: formatDate(dayjs(date).endOf("month"), "ddd"),
basic: formatDate(dayjs(date).endOf("month")),
object: dayjs(date).endOf("month"),
};
}
export function getDaysInMonth(date: string | dayjs.Dayjs) {
if (!isNaN(dayjs(date).daysInMonth())) {
return [...generateArrayNumber(1, dayjs(date).daysInMonth())];
}
return [];
}
export function nextMonth(date: dayjs.Dayjs) {
return date
.date(1)
.hour(0)
.minute(0)
.second(0)
.month(date.month() + 1);
}
export function previousMonth(date: dayjs.Dayjs) {
return date
.date(1)
.hour(0)
.minute(0)
.second(0)
.month(date.month() - 1);
}
export function getFirstElementsInArray(array: number[] = [], size = 0) {
return array.slice(0, size);
}
export function getLastElementsInArray(array: number[] = [], size = 0) {
const result: number[] = [];
if (Array.isArray(array) && size > 0) {
if (size >= array.length) {
return array;
}
let y = array.length - 1;
for (let i = 0; i < size; i++) {
result.push(array[y]);
y--;
}
}
return result.reverse();
}
export function getNumberOfDay(
dayString: string,
startWeekOn?: string | null | undefined
): number {
let number = 0;
let startDateModifier = 0;
if (startWeekOn) {
switch (startWeekOn) {
case "mon":
startDateModifier = 6;
break;
case "tue":
startDateModifier = 5;
break;
case "wed":
startDateModifier = 4;
break;
case "thu":
startDateModifier = 3;
break;
case "fri":
startDateModifier = 2;
break;
case "sat":
startDateModifier = 1;
break;
case "sun":
startDateModifier = 0;
break;
default:
break;
}
}
[
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
].forEach((item, index) => {
if (item.includes(dayString)) {
number = (index + startDateModifier) % 7;
}
});
return number;
}
export function getLastDaysInMonth(date: dayjs.Dayjs | string, size = 0) {
return getLastElementsInArray(getDaysInMonth(date), size);
}
export function getFirstDaysInMonth(date: string | dayjs.Dayjs, size = 0) {
return getFirstElementsInArray(getDaysInMonth(date), size);
}
export function loadLanguageModule(language = LANGUAGE) {
switch (language) {
case "af":
import("dayjs/locale/af");
break;
case "am":
import("dayjs/locale/am");
break;
case "ar-dz":
import("dayjs/locale/ar-dz");
break;
case "ar-iq":
import("dayjs/locale/ar-iq");
break;
case "ar-kw":
import("dayjs/locale/ar-kw");
break;
case "ar-ly":
import("dayjs/locale/ar-ly");
break;
case "ar-ma":
import("dayjs/locale/ar-ma");
break;
case "ar-sa":
import("dayjs/locale/ar-sa");
break;
case "ar-tn":
import("dayjs/locale/ar-tn");
break;
case "ar":
import("dayjs/locale/ar");
break;
case "az":
import("dayjs/locale/az");
break;
case "bg":
import("dayjs/locale/bg");
break;
case "bi":
import("dayjs/locale/bi");
break;
case "bm":
import("dayjs/locale/bm");
break;
case "bn-bd":
import("dayjs/locale/bn-bd");
break;
case "bn":
import("dayjs/locale/bn");
break;
case "bo":
import("dayjs/locale/bo");
break;
case "br":
import("dayjs/locale/br");
break;
case "ca":
import("dayjs/locale/ca");
break;
case "cs":
import("dayjs/locale/cs");
break;
case "cv":
import("dayjs/locale/cv");
break;
case "cy":
import("dayjs/locale/cy");
break;
case "da":
import("dayjs/locale/da");
break;
case "de-at":
import("dayjs/locale/de-at");
break;
case "de-ch":
import("dayjs/locale/de-ch");
break;
case "de":
import("dayjs/locale/de");
break;
case "dv":
import("dayjs/locale/dv");
break;
case "el":
import("dayjs/locale/el");
break;
case "en-au":
import("dayjs/locale/en-au");
break;
case "en-gb":
import("dayjs/locale/en-gb");
break;
case "en-ie":
import("dayjs/locale/en-ie");
break;
case "en-il":
import("dayjs/locale/en-il");
break;
case "en-in":
import("dayjs/locale/en-in");
break;
case "en-nz":
import("dayjs/locale/en-nz");
break;
case "en-sg":
import("dayjs/locale/en-sg");
break;
case "en-tt":
import("dayjs/locale/en-tt");
break;
case "en":
import("dayjs/locale/en");
break;
case "eo":
import("dayjs/locale/eo");
break;
case "es-do":
import("dayjs/locale/es-do");
break;
case "es-mx":
import("dayjs/locale/es-mx");
break;
case "es-pr":
import("dayjs/locale/es-pr");
break;
case "es-us":
import("dayjs/locale/es-us");
break;
case "es":
import("dayjs/locale/es");
break;
case "et":
import("dayjs/locale/et");
break;
case "eu":
import("dayjs/locale/eu");
break;
case "fa":
import("dayjs/locale/fa");
break;
case "fi":
import("dayjs/locale/fi");
break;
case "fo":
import("dayjs/locale/fo");
break;
case "fr-ch":
import("dayjs/locale/fr-ch");
break;
case "fr":
import("dayjs/locale/fr");
break;
case "fy":
import("dayjs/locale/fy");
break;
case "ga":
import("dayjs/locale/ga");
break;
case "gd":
import("dayjs/locale/gd");
break;
case "gl":
import("dayjs/locale/gl");
break;
case "gom-latn":
import("dayjs/locale/gom-latn");
break;
case "gu":
import("dayjs/locale/gu");
break;
case "he":
import("dayjs/locale/he");
break;
case "hi":
import("dayjs/locale/hi");
break;
case "hr":
import("dayjs/locale/hr");
break;
case "ht":
import("dayjs/locale/ht");
break;
case "hu":
import("dayjs/locale/hu");
break;
case "hy-am":
import("dayjs/locale/hy-am");
break;
case "id":
import("dayjs/locale/id");
break;
case "is":
import("dayjs/locale/is");
break;
case "it-ch":
import("dayjs/locale/it-ch");
break;
case "it":
import("dayjs/locale/it");
break;
case "ja":
import("dayjs/locale/ja");
break;
case "jv":
import("dayjs/locale/jv");
break;
case "ka":
import("dayjs/locale/ka");
break;
case "kk":
import("dayjs/locale/kk");
break;
case "ko":
import("dayjs/locale/ko");
break;
case "ku":
import("dayjs/locale/ku");
break;
case "ky":
import("dayjs/locale/ky");
break;
case "lb":
import("dayjs/locale/lb");
break;
case "lo":
import("dayjs/locale/lo");
break;
case "lt":
import("dayjs/locale/lt");
break;
case "lv":
import("dayjs/locale/lv");
break;
case "me":
import("dayjs/locale/me");
break;
case "mi":
import("dayjs/locale/mi");
break;
case "mk":
import("dayjs/locale/mk");
break;
case "ml":
import("dayjs/locale/ml");
break;
case "mn":
import("dayjs/locale/mn");
break;
case "ms-my":
import("dayjs/locale/ms-my");
break;
case "ms":
import("dayjs/locale/ms");
break;
case "mt":
import("dayjs/locale/mt");
break;
case "my":
import("dayjs/locale/my");
break;
case "nb":
import("dayjs/locale/nb");
break;
case "ne":
import("dayjs/locale/ne");
break;
case "nl-be":
import("dayjs/locale/nl-be");
break;
case "nl":
import("dayjs/locale/nl");
break;
case "nn":
import("dayjs/locale/nn");
break;
case "pa-in":
import("dayjs/locale/pa-in");
break;
case "pl":
import("dayjs/locale/pl");
break;
case "pt-br":
import("dayjs/locale/pt-br");
break;
case "pt":
import("dayjs/locale/pt");
break;
case "rn":
import("dayjs/locale/rn");
break;
case "ro":
import("dayjs/locale/ro");
break;
case "ru":
import("dayjs/locale/ru");
break;
case "rw":
import("dayjs/locale/rw");
break;
case "sd":
import("dayjs/locale/sd");
break;
case "se":
import("dayjs/locale/se");
break;
case "si":
import("dayjs/locale/si");
break;
case "sk":
import("dayjs/locale/sk");
break;
case "sl":
import("dayjs/locale/sl");
break;
case "sq":
import("dayjs/locale/sq");
break;
case "sr":
import("dayjs/locale/sr");
break;
case "sr-cyrl":
import("dayjs/locale/sr-cyrl");
break;
case "ss":
import("dayjs/locale/ss");
break;
case "sv-fi":
import("dayjs/locale/sv-fi");
break;
case "sv":
import("dayjs/locale/sv");
break;
case "sw":
import("dayjs/locale/sw");
break;
case "ta":
import("dayjs/locale/ta");
break;
case "te":
import("dayjs/locale/te");
break;
case "tg":
import("dayjs/locale/tg");
break;
case "th":
import("dayjs/locale/th");
break;
case "tk":
import("dayjs/locale/tk");
break;
case "tl-ph":
import("dayjs/locale/tl-ph");
break;
case "tlh":
import("dayjs/locale/tlh");
break;
case "tr":
import("dayjs/locale/tr");
break;
case "tzl":
import("dayjs/locale/tzl");
break;
case "tzm-latn":
import("dayjs/locale/tzm-latn");
break;
case "tzm":
import("dayjs/locale/tzm");
break;
case "ug-cn":
import("dayjs/locale/ug-cn");
break;
case "uk":
import("dayjs/locale/uk");
break;
case "ur":
import("dayjs/locale/ur");
break;
case "uz-latn":
import("dayjs/locale/uz-latn");
break;
case "uz":
import("dayjs/locale/uz");
break;
case "vi":
import("dayjs/locale/vi");
break;
case "x-pseudo":
import("dayjs/locale/x-pseudo");
break;
case "yo":
import("dayjs/locale/yo");
break;
case "zh-cn":
import("dayjs/locale/zh-cn");
break;
case "zh-hk":
import("dayjs/locale/zh-hk");
break;
case "zh-tw":
import("dayjs/locale/zh-tw");
break;
case "zh":
import("dayjs/locale/zh");
break;
default:
import("dayjs/locale/en");
break;
}
}
export function dateIsValid(date: Date | number) {
return date instanceof Date && !isNaN(date.getTime());
}

View File

@ -0,0 +1,24 @@
import React, { useEffect } from "react";
export default function useOnClickOutside(
ref: React.RefObject<HTMLDivElement>,
handler: (e?: MouseEvent | TouchEvent) => void
) {
useEffect(() => {
const listener = (event: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(event.target as Node)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}

View File

@ -0,0 +1,4 @@
import Datepicker from "./components/Datepicker";
export * from "./types";
export default Datepicker;

View File

@ -0,0 +1,97 @@
import React from "react";
import { COLORS } from "../constants";
export interface Period {
start: string | null;
end: string | null;
}
interface CustomShortcuts {
[key: string]: ShortcutsItem;
}
interface DefaultShortcuts {
today?: string;
yesterday?: string;
past?: (period: number) => string;
currentMonth?: string;
pastMonth?: string;
}
export interface Configs {
shortcuts?: DefaultShortcuts | CustomShortcuts;
footer?: {
cancel?: string;
apply?: string;
};
}
export interface ShortcutsItem {
text: string;
daysNumber?: number;
period: {
start: Date | string;
end: Date | string;
};
}
export type DateType = string | null | Date;
export type DateRangeType = {
startDate: DateType;
endDate: DateType;
};
export type DateValueType = DateRangeType | null;
export type ClassNamesTypeProp = {
container?: (p?: object | null | undefined) => string | undefined;
input?: (p?: object | null | undefined) => string | undefined;
toggleButton?: (p?: object | null | undefined) => string | undefined;
footer?: (p?: object | null | undefined) => string | undefined;
};
export type PopoverDirectionType = "up" | "down";
export interface DatepickerType {
primaryColor?: ColorKeys;
value: DateValueType;
onChange: (value: DateValueType, e?: HTMLInputElement | null | undefined) => void;
useRange?: boolean;
showFooter?: boolean;
showShortcuts?: boolean;
configs?: Configs;
asSingle?: boolean;
placeholder?: string;
separator?: string;
startFrom?: Date | null;
i18n?: string;
disabled?: boolean;
classNames?: ClassNamesTypeProp | undefined;
containerClassName?: ((className: string) => string) | string | null;
inputClassName?: ((className: string) => string) | string | null;
toggleClassName?: ((className: string) => string) | string | null;
toggleIcon?: (open: boolean) => React.ReactNode;
inputId?: string;
inputName?: string;
displayFormat?: string;
readOnly?: boolean;
minDate?: Date | null;
maxDate?: Date | null;
dateLooking?: "forward" | "backward" | "middle";
disabledDates?: DateRangeType[] | null;
startWeekOn?: string | null;
popoverDirection?: PopoverDirectionType;
mode?: "daily" | "monthly";
onMark?: (day: number, date: Date) => any;
onLoad?: () => Promise<void>
}
export type ColorKeys = (typeof COLORS)[number]; // "blue" | "orange"
export interface Colors {
[key: string]: {
[K in ColorKeys]: string;
};
}

622
components/ui/Document.tsx Normal file
View File

@ -0,0 +1,622 @@
"use client";
import React, { FC } from "react";
import {
Page,
Text,
View,
Document,
StyleSheet,
Font,
Image,
} from "@react-pdf/renderer";
import { Style } from "@react-pdf/types";
import { getNumber } from "@/lib/utils/getNumber";
Font.register({
family: "Noto Sans SC",
src: `${process.env.NEXT_PUBLIC_BASE_URL}/NotoSerifSC-Regular.ttf`,
});
Font.register({
family: "roboto",
src: `${process.env.NEXT_PUBLIC_BASE_URL}/Roboto-Medium.ttf`,
});
Font.register({
family: "roboto-light",
src: `${process.env.NEXT_PUBLIC_BASE_URL}/Roboto-Light.ttf`,
});
// Create styles
const styles = StyleSheet.create({
chinese: {
fontFamily: "Noto Sans SC", // Font Chinese
fontSize: 10,
},
page: {
padding: 20,
fontFamily: "roboto",
fontSize: 10,
display: "flex",
flexDirection: "column",
},
title: {
fontSize: 14,
textAlign: "center",
fontWeight: "bold",
},
thead: {
fontSize: 12,
textAlign: "center",
fontWeight: "bold",
display: "flex",
flexDirection: "row",
alignItems: "center",
},
section: {
marginBottom: 10,
},
table: {
width: "auto",
borderStyle: "solid",
borderWidth: 1,
marginBottom: 10,
},
tableRow: {
flexDirection: "row",
},
tableCol: {
borderStyle: "solid",
borderWidth: 1,
padding: 5,
flex: 1,
},
bold: {
fontWeight: "bold",
},
image: {
height: 40, // Atur tinggi gambar
marginBottom: 10,
alignSelf: "center",
},
});
const Row: FC<{
col1?: string;
col2?: string;
col3?: string;
col4?: string;
col5?: string;
style?: Style;
styleText?: Style;
footer?: boolean;
hideBorder?: boolean;
}> = ({
col1,
col2,
col3,
col4,
col5,
style,
styleText,
footer,
hideBorder,
}) => {
return (
<View
style={{
borderColor: "black",
display: "flex",
flexDirection: "row",
width: "100%",
...style,
}}
>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
borderRight: 1,
flexGrow: 1,
borderColor: "black",
justifyContent: footer ? "flex-end" : "flex-start",
borderBottom: 0,
padding: 5,
}}
>
{col1 && <Text style={styleText}>{col1}</Text>}
</View>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
width: 100,
borderRight: 1,
borderColor: "black",
borderTop: footer && !hideBorder ? 1 : 0,
borderBottom: footer && !hideBorder ? 1 : 0,
}}
>
{col2 && <Text style={styleText}>{col2}</Text>}
</View>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: 5,
width: 99.5,
borderRight: 1,
borderTop: footer && !hideBorder ? 1 : 0,
borderBottom: footer && !hideBorder ? 1 : 0,
}}
>
{col3 && <Text style={styleText}>{col3}</Text>}
</View>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: 100.5,
borderRight: 1,
borderColor: "black",
borderTop: footer && !hideBorder ? 1 : 0,
borderBottom: footer && !hideBorder ? 1 : 0,
}}
>
{col4 && <Text style={styleText}>{col4}</Text>}
</View>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
borderColor: "black",
width: 100,
borderTop: footer && !hideBorder ? 1 : 0,
borderBottom: footer && !hideBorder ? 1 : 0,
}}
>
{col5 && <Text style={styleText}>{col5}</Text>}
</View>
</View>
);
};
// Create Document Component
const convertRawData = (data: any): any[] => {
const formatGradeData = (grade: any): any[] => {
const exec = grade?.executive || [];
const non = grade?.non_executive || [];
const calculateSubTotal = (items: any[]): any => {
return {
existing: items.reduce((sum, item) => sum + (item?.existing || 0), 0),
promote: items.reduce((sum, item) => sum + (item?.promote || 0), 0),
recruit: items.reduce((sum, item) => sum + (item?.recruit || 0), 0),
total: items.reduce((sum, item) => sum + (item?.total || 0), 0),
};
};
const subTotalExecutive = calculateSubTotal(exec);
const subTotalNon = calculateSubTotal(non);
const rows: any[] = [];
const addRowData = (label: string, data: any[], subTotal: any): void => {
rows.push({ col1: label });
data.forEach((e) => {
rows.push({
col1: e?.job_level_name,
col2: e?.existing,
col3: e?.promote,
col4: e?.recruit,
col5: e?.total,
});
});
rows.push({
col1: "Sub - Total =",
col2: subTotal?.existing,
col3: subTotal?.promote,
col4: subTotal?.recruit,
col5: subTotal?.total,
});
};
if (exec.length) {
addRowData("Executives", exec, subTotalExecutive);
}
if (non.length) {
addRowData("Non-Executives", non, subTotalNon);
}
const totalRow = grade?.total?.[0] || {};
rows.push({
col1: "Total =",
col2: totalRow.existing || 0,
col3: totalRow.promote || 0,
col4: totalRow.recruit || 0,
col5: totalRow.total || 0,
});
return rows;
};
const processOverall = (overall: any, type: string, label: string): any => {
return {
type,
label,
operatingUnit: overall.operating_unit,
budgetYear: overall.budget_year,
rows: formatGradeData(overall.grade),
budgetRange: overall.budget_range,
existingDate: overall.existing_date,
};
};
const result: any[] = [];
if (data.overall) {
result.push(processOverall(data.overall, "overall", "Overall"));
}
if (data.organization_overall) {
data.organization_overall.forEach((org: any, index: number) => {
result.push(
processOverall(org.overall, "organization", `Organization ${index + 1}`)
);
org.location_overall.forEach((loc: any, locIndex: number) => {
result.push(
processOverall(
loc,
"location",
`Location ${index + 1}.${locIndex + 1}`
)
);
});
});
}
return result;
};
const MyDocument: FC<any> = ({ data }) => {
const page = convertRawData(data);
return (
<Document>
{page.map((page, pageIndex) => {
return (
<Page size="A4" style={styles.page} key={"page_" + pageIndex}>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
<Image
style={{ ...styles.image, marginRight: 10, marginLeft: 10 }}
src={`${process.env.NEXT_PUBLIC_BASE_URL}/julong.png`}
/>
<View style={styles.section}>
<Text style={styles.title}>JULONG GROUP (INDONESIA)</Text>
<Text
style={{ textAlign: "center", fontFamily: "Noto Sans SC" }}
>
</Text>
</View>
</View>
<View
style={{
display: "flex",
flexDirection: "row",
padding: 10,
flexGrow: 1,
}}
>
<View
style={{
display: "flex",
flexDirection: "column",
flexGrow: 1,
}}
>
<View
style={{
display: "flex",
flexDirection: "column",
width: "100%",
border: 1,
borderColor: "black",
}}
>
<View>
<View style={styles.section}>
<Text
style={{
...styles.title,
textDecoration: "underline",
marginBottom: 10,
}}
>
STAFF REQUIREMENT
</Text>
</View>
</View>
<View style={{ marginBottom: 10, padding: 5 }}>
<View
style={{
...styles.section,
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
<Text
style={{
fontSize: 12,
width: 100,
}}
>
OPERATING UNIT
</Text>
<Text
style={{
fontSize: 12,
paddingLeft: 10,
paddingRight: 10,
}}
>
:
</Text>
<Text
style={{
fontSize: 12,
paddingVertical: 1,
}}
>
{page?.operatingUnit}
</Text>
</View>
<View
style={{
...styles.section,
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
<Text
style={{
fontSize: 12,
width: 100,
}}
>
BUDGET YEAR
</Text>
<Text
style={{
fontSize: 12,
paddingLeft: 10,
paddingRight: 10,
}}
>
:
</Text>
<Text
style={{
fontSize: 12,
paddingVertical: 1,
}}
>
{page?.budgetYear}
</Text>
</View>
</View>
<View
style={{
borderBottom: 1,
borderTop: 1,
borderColor: "black",
display: "flex",
flexDirection: "row",
width: "100%",
}}
>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
padding: 5,
borderRight: 1,
flexGrow: 1,
borderColor: "black",
}}
>
<Text style={styles.thead}>Grade</Text>
</View>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
borderRight: 1,
borderColor: "black",
width: 100,
}}
>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: 5,
}}
>
<Text style={styles.thead}>Existing</Text>
</View>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexGrow: 1,
width: "100%",
borderTop: 1,
borderColor: "black",
}}
>
<Text style={styles.thead}>{page?.existingDate}</Text>
</View>
</View>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
borderRight: 1,
borderColor: "black",
width: 200,
}}
>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: 5,
}}
>
<Text style={styles.thead}>
{page?.budgetRange}
</Text>
</View>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
width: 200,
borderTop: 1,
borderColor: "black",
}}
>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: 5,
borderRight: 1,
width: 100,
borderColor: "black",
}}
>
<Text style={styles.thead}>Promote</Text>
</View>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
width: 100,
borderColor: "black",
}}
>
<Text style={styles.thead}>Recruit</Text>
</View>
</View>
</View>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
borderColor: "black",
width: 100,
}}
>
<View
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: 5,
}}
>
<Text style={styles.thead}>TOTAL</Text>
</View>
<View
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
}}
>
<Text style={styles.thead}>{page?.budgetYear}</Text>
</View>
</View>
</View>
{/* ROW */}
<Row col1=" " />
{page.rows.map((row: any, rowIndex: any) => (
<Row
key={"page_" + pageIndex + "_row_" + rowIndex}
styleText={{
fontFamily: [
"Executives",
"Non - Executives",
"Sub - Total =",
"Total =",
].includes(row?.col1)
? "roboto"
: "roboto-light",
fontWeight: [
"Executives",
"Non - Executives",
"Sub - Total =",
"Total =",
].includes(row?.col1)
? "bold"
: "light",
textDecoration: [
"Executives",
"Non - Executives",
].includes(row?.col1)
? "underline"
: "none",
}}
col1={row.col1}
col2={row.col2}
col3={row.col3}
col4={row.col4}
col5={row.col5}
footer={row?.col1 === "Sub - Total =" || row?.col1 === "Total =" ? true : false}
hideBorder={
row?.col1 === "Total =" ? true : false
}
/>
))}
</View>
</View>
</View>
</Page>
);
})}
</Document>
);
};
export default MyDocument;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,141 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils/utils"
import { buttonVariants } from "./button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-md text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

59
components/ui/alert.tsx Normal file
View File

@ -0,0 +1,59 @@
import { FC } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "./alert-dialog";
import { X } from "lucide-react";
export const Alert: FC<any> = ({
type,
onClick,
children,
className,
content,
msg
}) => {
const message: any = {
save: "Your data will be saved securely. You can update it at any time if needed.",
delete:
"This action cannot be undone. This will permanently remove your data from our servers.",
};
return (
<>
<AlertDialog>
<AlertDialogTrigger>{children}</AlertDialogTrigger>
<AlertDialogContent className={className}>
{content ? (
content
) : (
<>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
{message?.[type] || msg}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>No</AlertDialogCancel>
<AlertDialogAction
className={"bg-primary text-white"}
onClick={onClick}
>
Yes
</AlertDialogAction>
</AlertDialogFooter>
</>
)}
</AlertDialogContent>
</AlertDialog>
</>
);
};

36
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,61 @@
import { FC } from "react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "./breadcrumb";
import Link from "next/link";
const BreadcrumbBetter: FC<any> = ({ data }) => {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href="/">Home</Link>
</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
};
const BreadcrumbBetterLink: FC<{ data: any[]; className?: string }> = ({
data,
className,
}) => {
const item: any[] = addSeparator(data, ".");
return (
<Breadcrumb>
<BreadcrumbList className={cx(className)} >
{item.map((e, idx) => {
if (typeof e === "string" && e === ".")
return <BreadcrumbSeparator key={"separator_"+idx}/>;
return (
<BreadcrumbItem key={"item_"+idx}>
{e?.url ? (
<BreadcrumbLink asChild>
<Link href={e.url} className="hover:underline">{e.title}</Link>
</BreadcrumbLink>
) : (
<BreadcrumbPage>{e.title}</BreadcrumbPage>
)}
</BreadcrumbItem>
);
})}
</BreadcrumbList>
</Breadcrumb>
);
};
const addSeparator = (arr: any[], separator: any | string) => {
if (arr.length === 0) return [];
// Menggunakan flatMap untuk menyisipkan separator di antara elemen
return arr.flatMap((item, index) =>
index < arr.length - 1 ? [item, separator] : [item]
);
};
export { BreadcrumbBetter, BreadcrumbBetterLink };

View File

@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1 break-words text-sm text-muted-foreground sm:gap-1",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -0,0 +1,69 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/utils";
import Link from "next/link";
import { FC } from "react";
const btn = cva(
"px-4 py-2 group relative flex items-stretch justify-center p-0.5 text-center font-medium transition-[color,background-color,border-color,text-decoration-color,fill,stroke,box-shadow] focus:z-10 focus:outline-none border border-transparent text-white focus:ring-4 focus:ring-cyan-300 enabled:hover:bg-cyan-800 dark:bg-cyan-600 dark:focus:ring-cyan-800 dark:enabled:hover:bg-cyan-700 rounded-lg"
);
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const ButtonBetter = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
const ButtonLink: FC<any> = ({ className, children, href }) => {
return (
<Link href={href}>
<ButtonBetter className={cx(className, "text-white")}>{children}</ButtonBetter>
</Link>
);
};
ButtonBetter.displayName = "Button";
export {ButtonLink };

71
components/ui/button.tsx Normal file
View File

@ -0,0 +1,71 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils/utils";
import { FC } from "react";
const btn = cva(
" text-white px-4 py-1.5 group active-menu-icon relative flex items-stretch justify-center p-0.5 text-center border border-transparent text-white enabled:hover:bg-cyan-800 rounded-md"
);
const buttonVariants = cva(
"cursor-pointer px-4 py-1.5 group relative flex items-stretch justify-center p-0.5 text-center border inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-white shadow hover:bg-primary/90 active-menu-icon",
reject:
"bg-red-500 text-white shadow hover:bg-red-500 active-menu-icon",
destructive:
"bg-red-500 text-white shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const ButtonBetter = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
const ButtonContainer: FC<any> = ({ children, className, variant = "default" }) => {
const vr = variant ? variant: "default"
return (
<div className={cx(buttonVariants({ variant: vr, className }))}>
<div className="flex items-center gap-x-0.5 text-sm">{children}</div>
</div>
);
};
ButtonBetter.displayName = "Button";
export { ButtonBetter, ButtonContainer, buttonVariants, btn };

76
components/ui/card.tsx Normal file
View File

@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

251
components/ui/carousel.tsx Normal file
View File

@ -0,0 +1,251 @@
"use client"
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils/utils"
import { Button } from "flowbite-react"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext]
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
}
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
// variant={variant}
size={size}
className={cn(
"h-8 w-8 rounded-full text-black items-center"
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
size={size}
className={cn(
"h-8 w-8 rounded-full text-black items-center",
className
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-white",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

122
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,38 @@
import * as React from "react";
import { cn } from "@/lib/utils/utils";
import { HiSearch } from "react-icons/hi";
const InputSearch = React.forwardRef<
HTMLInputElement,
React.ComponentProps<"input">
>(({ className, type, ...props }, ref) => {
return (
<div className="flex flex-row relative">
<HiSearch
className={cx(
"absolute",
css`
top: 50%;
left: 17px;
transform: translate(-50%, -50%);
`
)}
/>
<input
type={type}
className={cn(
"px-3 py-2 flex h-9 w-full rounded-md border border-gray-300 border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className,
css`
padding-left:30px`
)}
ref={ref}
{...props}
/>
</div>
);
});
InputSearch.displayName = "Input";
export { InputSearch };

22
components/ui/input.tsx Normal file
View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-gray-200 border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:border-gray-200/50 md:text-sm",
className,
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,38 @@
import Link from "next/link";
import { FC } from "react";
export const SidebarLinkBetter: FC<{
className: string;
style: any;
children: any;
href?: string;
onClick?: () => void;
}> = ({ className, style, children, href, onClick }) => {
if (href)
return (
<div
className="flex flex-row flex-grow w-full"
onClick={() => {
if (typeof onClick === "function") {
onClick();
}
}}
>
<Link
href={href}
className={className}
style={style} // Terapkan gaya berdasarkan depth
>
{children}
</Link>
</div>
);
return (
<div
className={className}
style={style} // Terapkan gaya berdasarkan depth
>
{children}
</div>
);
};

View File

@ -0,0 +1,36 @@
import { FC } from "react";
import { Copy } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "./dialog";
export const PreviewImagePopup: FC<any> = ({ url, className, children }) => {
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-5xl h-4/5 flex flex-col ">
<DialogHeader>
<DialogTitle>Preview</DialogTitle>
<DialogDescription className="hidden">
Anyone who has this link will be able to view this.
</DialogDescription>
</DialogHeader>
<div className="flex items-center flex-row space-x-2 bg-black flex-grow">
<div
className={cx(
"flex h-full flex-grow flex-row bg-no-repeat bg-center bg-contain",
css`
background-image: url("${url}");
`
)}
></div>
</div>
</DialogContent>
</Dialog>
);
};

45
components/ui/resize.tsx Normal file
View File

@ -0,0 +1,45 @@
"use client"
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -0,0 +1,9 @@
import { Loader2 } from "lucide-react";
export const Spinner = ({ className }: { className?: string }) => {
return (
<div className={className}>
<Loader2 className={cx("h-4 w-4 animate-spin")} />
</div>
);
};

55
components/ui/tabs.tsx Normal file
View File

@ -0,0 +1,55 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { cn } from "@/lib/utils/utils";
const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"rounded-t-lg relative inline-flex items-center justify-center whitespace-nowrap text-black bg-white data-[state=active]:bg-primary data-[state=active]:text-white px-6 py-1 text-md font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow data-[state=active]:active-tab border-x border-t border-b-none data-[state=active]:z-0 ",
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"flex-grow ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };

102
components/ui/tabslider.tsx Normal file
View File

@ -0,0 +1,102 @@
"use client";
import React, { useRef, useEffect, useState, useCallback } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { FaAngleLeft, FaAngleRight } from "react-icons/fa6";
const TabSlider: React.FC<any> = ({
children,
className,
disabledPagination,
}) => {
// const containerRef = useRef<HTMLDivElement>(null);
const [canScrollNext, setCanScrollNext] = useState(false);
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [emblaRef, emblaApi] = useEmblaCarousel({});
const onSelect = useCallback(() => {
if (!emblaApi) return;
setCanScrollNext(emblaApi.canScrollNext());
setCanScrollPrev(emblaApi.canScrollPrev());
}, [emblaApi]);
const wrapper = useRef<HTMLDivElement>(null);
const item = useRef<HTMLDivElement>(null);
const [widthWrapper, setWidthWrapper] = useState(0);
const [isScroll, setIsScroll] = useState(false);
const [ready, setReady] = useState(false);
useEffect(() => {
if (wrapper.current) {
setWidthWrapper(wrapper?.current?.clientWidth);
setTimeout(() => {
setReady(true);
}, 1000);
}
}, [wrapper]);
useEffect(() => {
if (item.current) {
if (item.current.scrollWidth > widthWrapper) {
setIsScroll(true);
}
}
}, [item.current]);
useEffect(() => {
if (!emblaApi) return;
emblaApi.on("select", onSelect);
onSelect(); // Initialize state
}, [emblaApi, onSelect]);
return (
<div className="flex flex-grow flex-row w-full">
<div
className={cx(
"relative flex flex-row items-center w-full pt-2",
className
)}
// ref={containerRef}
>
{!disabledPagination && (
<button
className={cx(
"top-0 left-0 p-1 mx-0.5 bg-gray-50/40 text-gray-800 rounded-lg flex flex-row items-center justify-center w-6 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600",
isScroll ? "visible" : "invisible"
)}
onClick={() => emblaApi && emblaApi.scrollPrev()}
>
<FaAngleLeft />
</button>
)}
<div className="flex flex-grow" ref={wrapper}>
{ready ? (
<div
className={cx("overflow-hidden flex-grow")}
style={{
width: widthWrapper,
}}
ref={emblaRef}
>
<div className="flex flex-shrink-0" ref={item}>
{children}
</div>
</div>
) : (
<></>
)}
</div>
{!disabledPagination && (
<button
className={cx(
"top-0 left-0 p-1 mx-0.5 bg-gray-50/40 text-gray-800 rounded-lg flex flex-row items-center justify-center w-6 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600",
isScroll ? "visible" : "invisible"
)}
onClick={() => emblaApi && emblaApi.scrollNext()}
>
<FaAngleRight />
</button>
)}
</div>
</div>
);
};
export default TabSlider;

View File

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[60px] w-full disabled:bg-gray-100/50 border-gray-300 rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }

View File

@ -0,0 +1,76 @@
"use client";
import type { PropsWithChildren } from "react";
import { createContext, useContext, useEffect, useState } from "react";
import isBrowser from "../helpers/is-browser";
import isSmallScreen from "../helpers/is-small-screen";
interface SidebarContextProps {
isOpenOnSmallScreens: boolean;
isPageWithSidebar: boolean;
// eslint-disable-next-line no-unused-vars
setOpenOnSmallScreens: (isOpen: boolean) => void;
}
const SidebarContext = createContext<SidebarContextProps>(undefined!);
export function SidebarProvider({ children }: PropsWithChildren) {
const location = isBrowser() ? window.location.pathname : "/";
const [isOpen, setOpen] = useState(
isBrowser()
? window.localStorage.getItem("isSidebarOpen") === "true"
: false
);
// Save latest state to localStorage
useEffect(() => {
window.localStorage.setItem("isSidebarOpen", isOpen.toString());
}, [isOpen]);
// Close Sidebar on page change on mobile
useEffect(() => {
if (isSmallScreen()) {
setOpen(false);
}
}, [location]);
// Close Sidebar on mobile tap inside main content
useEffect(() => {
function handleMobileTapInsideMain(event: MouseEvent) {
const main = document.querySelector("main");
const isClickInsideMain = main?.contains(event.target as Node);
if (isSmallScreen() && isClickInsideMain) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleMobileTapInsideMain);
return () => {
document.removeEventListener("mousedown", handleMobileTapInsideMain);
};
}, []);
return (
<SidebarContext.Provider
value={{
isOpenOnSmallScreens: isOpen,
isPageWithSidebar: true,
setOpenOnSmallScreens: setOpen,
}}
>
{children}
</SidebarContext.Provider>
);
}
export function useSidebarContext(): SidebarContextProps {
const context = useContext(SidebarContext);
if (typeof context === "undefined") {
throw new Error(
"useSidebarContext should be used within the SidebarContext provider!"
);
}
return context;
}

5
helpers/is-browser.ts Normal file
View File

@ -0,0 +1,5 @@
function isBrowser(): boolean {
return typeof window !== "undefined";
}
export default isBrowser;

View File

@ -0,0 +1,7 @@
import isBrowser from "./is-browser";
function isSmallScreen(): boolean {
return isBrowser() && window.innerWidth < 768;
}
export default isSmallScreen;

79
utils/action.tsx Normal file
View File

@ -0,0 +1,79 @@
import get from "lodash.get";
import { AlertTriangle, Check, Loader2 } from "lucide-react";
import { toast } from "sonner";
export const actionToast = async (data: {
task: () => Promise<any>;
before?: () => any;
success?: () => any;
after?: () => any;
msg_succes?: string;
msg_error?: string;
msg_load?: string;
}) => {
const { task, before, after, success, msg_succes, msg_error, msg_load } =
data;
try {
if (typeof before === "function") before();
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);
}
}
`
)}
/>
{msg_load ? msg_load : " Load..."}
</>
);
if (typeof task === "function") await task();
setTimeout(() => {
toast.success(
<div
className={cx(
"cursor-pointer flex flex-col select-none items-stretch flex-1 w-full"
)}
onClick={() => {
toast.dismiss();
}}
>
<div className="flex text-green-700 items-center success-title font-semibold">
<Check className="h-6 w-6 mr-1 " />
{msg_succes ? msg_succes : "Success"}
</div>
</div>
);
if (typeof after === "function") after();
if (typeof success === "function") success();
}, 1000);
} catch (ex: any) {
toast.error(
<div className="flex flex-col w-full">
<div className="flex text-red-600 items-center">
<AlertTriangle className="h-4 w-4 mr-1" />
{msg_error ? msg_error : "Failed"} { get(ex, "response.data.meta.message") || ex.message}.
</div>
</div>,
{
dismissible: true,
className: css`
background: #ffecec;
border: 2px solid red;
`,
}
);
if (typeof after === "function") after();
}
};

30
utils/axios.ts Normal file
View File

@ -0,0 +1,30 @@
import axios from "axios";
import Cookies from "js-cookie";
import dotenv from 'dotenv';
dotenv.config();
// Buat instance Axios
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || "https://api.example.com", // Ganti dengan URL API Anda
timeout: 10000, // Timeout dalam milidetik
withCredentials: true, // Kirim cookie otomatis dengan setiap permintaan
});
// Interceptor untuk menambahkan token dari cookie ke header Authorization (jika diperlukan)
api.interceptors.request.use((config) => {
const token = Cookies.get("token"); // Ambil token dari cookie
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Fungsi untuk memperbarui token jika diperlukan secara manual
export const setAuthToken = (token: string) => {
if (token) {
api.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} else {
delete api.defaults.headers.common["Authorization"];
}
};
export default api;

7
utils/cloneFm.ts Normal file
View File

@ -0,0 +1,7 @@
export const cloneFM = (fm: any, row: any) => {
// const result -
return {
...fm,
data: row
}
}

99
utils/conditionalMPR.ts Normal file
View File

@ -0,0 +1,99 @@
"use client";
import get from "lodash.get";
import { get_user } from "./get_user";
import { fa } from "@faker-js/faker";
export const showApprovel = (
data: any,
permision: string[],
action?: "approve" | "reject"
) => {
if (!permision?.length) {
return null;
}
const a1 = [
{
status: "IN PROGRESS",
permision: ["approval-mpr-dept-head"],
column: ["department_head"],
level: ["Level Head Department"],
},
{
status: "NEED APPROVAL",
permision: [
"approval-mpr-dept-head",
"approval-mpr-vp",
"approval-mpr-ceo",
],
column: ["department_head", "vp_gm_director", "ceo"],
level: ["Level Head Department", "Level VP", "Level CEO"],
},
{
status: "APPROVED",
permision: ["approval-mpr-ho"],
column: ["hrd_ho_unit"],
level: ["Level HRD HO"],
},
]; // tiga status yang dapat memunculkan approval
const role = {
head: permision.find((e) => e === "approval-mpr-dept-head"),
dir: permision.find((e) => e === "approval-mpr-vp"),
ceo: permision.find((e) => e === "approval-mpr-ceo"),
ho_unit: permision.find((e) => e === "approval-mpr-ho"),
};
const isBudget = data?.mp_planning_header_id ? true : false;
const isField = data?.organization_category === "Non Field" ? false : true;
if (data?.status === "NEED APPROVAL") {
if (data?.department_head && !data?.vp_gm_director) {
return {
approve:
action === "reject"
? "REJECTED"
: isField
? "APPROVED"
: "NEED APPROVAL",
level: "Level VP",
};
} else if (data?.vp_gm_director && !data?.ceo) {
return null;
return {
approve: action === "reject" ? "REJECTED" : "APPROVED",
level: "Level VP",
};
}
} else if (data?.status === "IN PROGRESS") {
const isYou = data?.requestor_id === get_user("m_employee.id");
if (isYou) {
return {
approve:
action === "reject"
? "REJECTED"
: isBudget
? "APPROVED"
: "NEED APPROVAL",
level: "Level Head Department",
};
}
return null;
} else if (data?.status === "APPROVED") {
console.log(data?.status)
if (data?.department_head && !data?.vp_gm_director) {
return {
approve:
action === "reject"
? "REJECTED"
: isField
? "APPROVED"
: "NEED APPROVAL",
level: "Level VP",
};
} else if (!data?.hrd_ho_unit) {
return {
approve: action === "reject" ? "REJECTED" : "COMPLETED",
level: "Level HRD HO",
};
}
}
return null;
};

3
utils/cx.ts Normal file
View File

@ -0,0 +1,3 @@
import clsx from "clsx";
export default clsx;

48
utils/date.ts Normal file
View File

@ -0,0 +1,48 @@
import day from "dayjs";
import relative from "dayjs/plugin/relativeTime";
day.extend(relative);
export const longDate = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
return day(date).format("DD MMM YYYY - hh:mm");
}
return "-";
};
export const dayDate = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
return day(date).format("DD MMMM YYYY");
}
return "-";
};
export const shortDate = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
const formattedDate = day(date);
if (formattedDate.isValid()) {
return formattedDate.format("DD/MM/YYYY");
}
}
return "-";
};
export const normalDate = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
return day(date).format("YYYY-MM-DD");
}
return null;
};
export const timeAgo = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
return day(date).fromNow();
}
return "-";
};
export const formatTime = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
return day(date).format("hh:mm");
}
return "-";
};

17
utils/debounceHandler.ts Normal file
View File

@ -0,0 +1,17 @@
export const debouncedHandler = <T>(
callback: () => void | Promise<void>, // Bisa sinkron atau async
delay: number
) => {
let timeout: NodeJS.Timeout;
return () => {
clearTimeout(timeout);
timeout = setTimeout(async () => {
try {
await callback();
} catch (error) {
console.error("Error in debounced function:", error);
}
}, delay);
};
};

5
utils/detectCase.ts Normal file
View File

@ -0,0 +1,5 @@
export const detectCase = (url: string, target: string): boolean => {
// Buat regex untuk mendeteksi target dan sub-path-nya
const regex = new RegExp(`^${target}(?:/|$)`);
return regex.test(url);
};

25
utils/event.ts Normal file
View File

@ -0,0 +1,25 @@
import get from "lodash.get";
import { generateQueryString } from "./generateQueryString";
type EventActions = "before-onload" | "onload-param" | string;
export const events = async (action: EventActions, data: any) => {
switch (action) {
case "onload-param":
const params = {
...data,
page: get(data, "paging"),
page_size: get(data, "take"),
search: get(data, "search")
};
delete params["paging"]
delete params["take"]
return generateQueryString(params)
return
break;
default:
break;
}
return null
}

View File

@ -0,0 +1,47 @@
import get from "lodash.get";
export const filterMenuByPermission = (menuConfig: any[], permision: any[]) => {
const userPermissions = permision?.length
? permision.map((e) => get(e, "name"))
: [];
return menuConfig
.map((menu) => {
// Filter children berdasarkan permission user
const filteredChildren = menu.children?.filter((child: any) =>
child.permision.some((perm: any) => userPermissions.includes(perm))
);
// Periksa apakah menu utama atau salah satu child memiliki permission yang sesuai
const hasPermission =
menu.permision.some((perm: any) => userPermissions.includes(perm)) ||
(filteredChildren && filteredChildren.length > 0);
// Jika cocok, kembalikan menu dengan children yang sudah difilter
if (hasPermission) {
return {
...menu,
children: filteredChildren,
};
}
return null; // Tidak menyertakan menu yang tidak memiliki permission
})
.filter((menu) => menu !== null); // Hapus menu yang null
};
export const getFirstMenuWithUrl = (menuConfig: any[]): any | null => {
for (const menu of menuConfig) {
// Jika menu memiliki href langsung, kembalikan menu tersebut
if (menu.href) {
return menu?.href;
}
// Jika menu memiliki children, cari secara rekursif
if (menu.children) {
const childWithUrl = getFirstMenuWithUrl(menu.children);
if (childWithUrl) {
return childWithUrl;
}
}
}
// Jika tidak ada menu dengan URL, kembalikan null
return null;
};

View File

@ -0,0 +1,25 @@
export const generateQueryString = (data: Record<string, any>) => {
// Priksa menapa input punika bentukipun object lan mboten null
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
return ""
}
// Nyaring nilai null utawi undefined, lajeng priksa menapa object punika kosong sasampunipun disaring
const filteredData = Object.keys(data)
.filter(key => data[key] !== undefined && data[key] !== null)
.reduce((obj: any, key) => {
obj[key] = data[key];
return obj;
}, {});
if (Object.keys(filteredData).length === 0) {
return ''; // Wangsul string kosong menawi object ingkang sampun disaring kosong
}
// Nggawe query string saking data ingkang sampun disaring
const queryString = Object.keys(filteredData)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(filteredData[key])}`)
.join('&');
return "?" +queryString;
}

6
utils/get-params.ts Normal file
View File

@ -0,0 +1,6 @@
import { useParams } from "next/navigation";
export const getParams = (name: string) => {
const params = useParams();
return params?.[name]
}

50
utils/getAccess.ts Normal file
View File

@ -0,0 +1,50 @@
import get from "lodash.get";
import { get_user } from "./get_user";
import api from "./axios";
export const getAccess = (keys: string, data: any[]) => {
if (!Array.isArray(data) || !data?.length) return false;
for (const role of data) {
const permissionExists = role.permissions.some(
(permission: any) => get(permission, "name") === keys
);
if (permissionExists) {
return true;
}
}
return false;
};
export const userRoleMe = async (w?: boolean) => {
let user = null as any;
let data = null as any;
if (w && typeof window === "object") {
user = get(window, "user");
data = user;
} else {
user = await api.get(`${process.env.NEXT_PUBLIC_API_PORTAL}/api/users/me`);
data = user?.data.data;
}
const choosed_role = data?.choosed_role;
const roles = data.roles;
if (!roles?.length) return [];
return roles.filter((e: any) => e?.name === choosed_role) || [];
};
export const accessMe = async (keys: string) => {
const user = await api.get(
`${process.env.NEXT_PUBLIC_API_PORTAL}/api/users/me`
);
const data = user?.data.data;
const roles = data.roles;
if (!Array.isArray(roles) || !roles?.length) return false;
for (const role of roles) {
const permissionExists = role.permissions.some(
(permission: any) => get(permission, "name") === keys
);
if (permissionExists) {
return true;
}
}
return false;
};

3
utils/getNumber.ts Normal file
View File

@ -0,0 +1,3 @@
export const getNumber = (data: any) => {
return Number(data) || 0;
};

5
utils/getParamsUrl.ts Normal file
View File

@ -0,0 +1,5 @@
export const get_params_url = (name: string) => {
const urlParams = new URLSearchParams(window.location.search);
const result = urlParams.get(name);
return result
}

54
utils/getStatusDate.ts Normal file
View File

@ -0,0 +1,54 @@
export const getStatus = (
startDate: string,
endDate: string,
currentDate: string = new Date().toISOString()
): string | null => {
try {
// Validasi input tanggal
if (!isValidDate(startDate) || !isValidDate(endDate)) {
return null
}
// Konversi tanggal ke format YYYY-MM-DD
const formattedStartDate = formatDateToYYYYMMDD(startDate);
const formattedEndDate = formatDateToYYYYMMDD(endDate);
const formattedCurrentDate = formatDateToYYYYMMDD(currentDate);
// Tentukan status
if (formattedCurrentDate === formattedStartDate) {
return 'open';
} else if (formattedCurrentDate > formattedStartDate && formattedCurrentDate <= formattedEndDate) {
return 'in progress';
} else if (formattedCurrentDate > formattedEndDate) {
return 'closed';
} else {
return 'pending';
}
} catch (error: unknown) {
// Log error dan kembalikan pesan error standar
console.error(error);
return null
}
};
export const formatDateToYYYYMMDD = (dateString: string): string => {
try {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
throw new Error("Invalid date provided for formatting.");
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-based
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
} catch (error: unknown) {
console.error(error);
throw new Error("Failed to format date.");
}
};
export const isValidDate = (dateString: string): boolean => {
const date = new Date(dateString);
return !isNaN(date.getTime());
};

5
utils/getValue.ts Normal file
View File

@ -0,0 +1,5 @@
import get from "lodash.get"
export const getValue = (data: any, keys: string) => {
return get(data, keys) ? get(data, keys) : ""
}

9
utils/get_user.ts Normal file
View File

@ -0,0 +1,9 @@
import get from "lodash.get";
export const get_user = (name?: string) => {
if(typeof window !== "object") return null
const w = window as unknown as {
user: any;
};
return name ? get(w.user, name) : w?.user;
};

6
utils/isStringEmpty.ts Normal file
View File

@ -0,0 +1,6 @@
export const isStringEmpty = (input: string | null | undefined): boolean => {
if (input === null || input === undefined) {
return true;
}
return input.trim().length === 0;
};

9
utils/joinString.ts Normal file
View File

@ -0,0 +1,9 @@
export const joinString = (data: any[], keys: string): string => {
// Jika array kosong, tambahkan elemen baru
if(!Array.isArray(data)) return ""
if (!data.length) return "";
// Pastikan nilai `name` unik
const uniqueNames = new Set(data.map((item) => item?.[keys]));
// Gabungkan semua `name` menjadi string
return data.map((item) => item.name).join(", ");
}

48
utils/makeData.ts Normal file
View File

@ -0,0 +1,48 @@
import { faker } from '@faker-js/faker'
export type Person = {
firstName: string
lastName: string
age: number
visits: number
progress: number
status: 'relationship' | 'complicated' | 'single'
subRows?: Person[]
}
const range = (len: number) => {
const arr: number[] = []
for (let i = 0; i < len; i++) {
arr.push(i)
}
return arr
}
const newPerson = (): Person => {
return {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
age: faker.number.int(40),
visits: faker.number.int(1000),
progress: faker.number.int(100),
status: faker.helpers.shuffle<Person['status']>([
'relationship',
'complicated',
'single',
])[0]!,
}
}
export function makeData(...lens: number[]) {
const makeDataLevel = (depth = 0): Person[] => {
const len = lens[depth]!
return range(len).map((d): Person => {
return {
...newPerson(),
subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
}
})
}
return makeDataLevel()
}

21
utils/navigate.ts Normal file
View File

@ -0,0 +1,21 @@
"use client";
import { useRouter } from "next/navigation";
export const navigate = (path: string) => {
if (!router) throw new Error("Router instance is not set.");
router.push(path);
};
// Fungsi navigasi dengan query parameter
export const navigateWithQuery = (path: string, query: Record<string, string>) => {
if (!router) throw new Error("Router instance is not set.");
const url = new URL(window.location.origin + path);
Object.keys(query).forEach((key) => url.searchParams.append(key, query[key]));
router.push(url.toString());
};
// Fungsi replace
export const replace = (path: string) => {
if (!router) throw new Error("Router instance is not set.");
router.replace(path);
};

5
utils/siteurl.ts Normal file
View File

@ -0,0 +1,5 @@
import dotenv from 'dotenv';
dotenv.config();
export const siteurl = (param: string) => {
return `${process.env.NEXT_PUBLIC_BASE_URL + param}`
}

0
utils/toast.ts Normal file
View File

104
utils/use-local.ts Normal file
View File

@ -0,0 +1,104 @@
import { useEffect, useRef, useState } from "react";
export const useLocal = <T extends object>(
data: T,
effect?: (arg: {
init: boolean;
setDelayedRender: (arg: boolean) => void;
}) => Promise<void | (() => void)> | void | (() => void),
deps?: any[]
): {
[K in keyof T]: T[K] extends Promise<any> ? null | Awaited<T[K]> : T[K];
} & { render: () => void } => {
const [, _render] = useState({});
const _ = useRef({
data: data as unknown as T & {
render: () => void;
},
deps: (deps || []) as any[],
ready: false,
_loading: {} as any,
lastRender: 0,
lastRenderCount: 0,
delayedRender: false,
delayedRenderTimeout: null as any,
overRenderTimeout: null as any,
});
const local = _.current;
useEffect(() => {
local.ready = true;
if (effect)
effect({
init: true,
setDelayedRender(arg) {
local.delayedRender = arg;
},
});
}, []);
if (local.ready === false) {
local._loading = {};
local.data.render = () => {
if (local.ready) {
if (local.delayedRender) {
if (Date.now() - local.lastRender > 100) {
local.lastRender = Date.now();
_render({});
} else {
clearTimeout(local.delayedRenderTimeout);
local.delayedRenderTimeout = setTimeout(local.data.render, 50);
}
return;
}
if (Date.now() - local.lastRender < 300) {
local.lastRenderCount++;
} else {
local.lastRenderCount = 0;
}
local.lastRender = Date.now();
if (local.lastRenderCount > 300) {
clearTimeout(local.overRenderTimeout);
local.overRenderTimeout = setTimeout(() => {
local.lastRenderCount = 0;
local.lastRender = Date.now();
_render({});
}, 1000);
console.error(
`local.render executed ${local.lastRenderCount} times in less than 300ms`
);
return;
}
_render({});
}
};
} else {
if (local.deps.length > 0 && deps) {
for (const [k, dep] of Object.entries(deps) as any) {
if (local.deps[k] !== dep) {
local.deps[k] = dep;
if (effect) {
setTimeout(() => {
effect({
init: false,
setDelayedRender(arg) {
local.delayedRender = arg;
},
});
});
}
break;
}
}
}
}
return local.data as any;
};

Some files were not shown because too many files have changed in this diff Show More