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