first commit
This commit is contained in:
commit
a1612f527a
|
|
@ -0,0 +1,4 @@
|
||||||
|
[data-floating-ui-portal] > div {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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 don’t 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;
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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 || <> </>}
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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 || <> </>}
|
||||||
|
</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 ? (
|
||||||
|
<>— Empty —</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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">
|
||||||
|
© 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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
<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>
|
||||||
|
and
|
||||||
|
<span className="font-medium text-gray-900 ">5 others</span>
|
||||||
|
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>
|
||||||
|
and
|
||||||
|
<span className="font-medium text-gray-900 ">141 others</span>
|
||||||
|
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>
|
||||||
|
mentioned you in a comment:
|
||||||
|
<span className="font-medium text-primary-700 ">
|
||||||
|
@bonnie.green
|
||||||
|
</span>
|
||||||
|
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>
|
||||||
|
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;
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
const Script = () => {
|
||||||
|
return (
|
||||||
|
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Script;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 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
|
||||||
|
<span className="font-semibold text-gray-900">{page}</span>
|
||||||
|
of
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
export const init_column = (data: any[]) => {
|
||||||
|
return data.length
|
||||||
|
? data.map((e) => {
|
||||||
|
return {
|
||||||
|
accessorKey: e.name,
|
||||||
|
...e,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
import Datepicker from "./components/Datepicker";
|
||||||
|
|
||||||
|
export * from "./types";
|
||||||
|
export default Datepicker;
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
@ -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 }
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 }
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
function isBrowser(): boolean {
|
||||||
|
return typeof window !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default isBrowser;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import isBrowser from "./is-browser";
|
||||||
|
|
||||||
|
function isSmallScreen(): boolean {
|
||||||
|
return isBrowser() && window.innerWidth < 768;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default isSmallScreen;
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const cloneFM = (fm: any, row: any) => {
|
||||||
|
// const result -
|
||||||
|
return {
|
||||||
|
...fm,
|
||||||
|
data: row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
export default clsx;
|
||||||
|
|
@ -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 "-";
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
|
export const getParams = (name: string) => {
|
||||||
|
const params = useParams();
|
||||||
|
return params?.[name]
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const getNumber = (data: any) => {
|
||||||
|
return Number(data) || 0;
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import get from "lodash.get"
|
||||||
|
|
||||||
|
export const getValue = (data: any, keys: string) => {
|
||||||
|
return get(data, keys) ? get(data, keys) : ""
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const isStringEmpty = (input: string | null | undefined): boolean => {
|
||||||
|
if (input === null || input === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return input.trim().length === 0;
|
||||||
|
};
|
||||||
|
|
@ -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(", ");
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
export const siteurl = (param: string) => {
|
||||||
|
return `${process.env.NEXT_PUBLIC_BASE_URL + param}`
|
||||||
|
}
|
||||||
|
|
@ -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
Loading…
Reference in New Issue