banyak bos
This commit is contained in:
parent
bb0e6d38f1
commit
31680f29c7
|
|
@ -46,7 +46,9 @@ export function usePopover({
|
|||
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>();
|
||||
const [descriptionId, setDescriptionId] = React.useState<
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
// Determine whether the popover is open
|
||||
const open = controlledOpen ?? uncontrolledOpen;
|
||||
|
|
@ -113,7 +115,6 @@ export function usePopover({
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
function mapPlacementSideToCSSProperty(placement: Placement) {
|
||||
const staticPosition = placement.split("-")[0];
|
||||
|
||||
|
|
@ -186,6 +187,7 @@ export function Popover({
|
|||
className,
|
||||
classNameTrigger,
|
||||
arrow,
|
||||
popoverClassName,
|
||||
...restOptions
|
||||
}: {
|
||||
root?: HTMLElement;
|
||||
|
|
@ -193,6 +195,7 @@ export function Popover({
|
|||
classNameTrigger?: string;
|
||||
children: React.ReactNode;
|
||||
content?: React.ReactNode;
|
||||
popoverClassName?: string;
|
||||
arrow?: boolean;
|
||||
} & PopoverOptions) {
|
||||
const popover = usePopover({ modal, ...restOptions });
|
||||
|
|
@ -216,14 +219,17 @@ export function Popover({
|
|||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={cx(
|
||||
popoverClassName
|
||||
? popoverClassName
|
||||
: 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 />}
|
||||
|
|
@ -269,7 +275,6 @@ export const PopoverTrigger = React.forwardRef<
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
export const PopoverContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLProps<HTMLDivElement>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } 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";
|
||||
import { TypeRichText } from "./field/TypeRichText";
|
||||
import { TypeTag } from "./field/TypeTag";
|
||||
import get from "lodash.get";
|
||||
import { getNumber } from "@/lib/utils/getNumber";
|
||||
|
||||
export const Field: React.FC<any> = ({
|
||||
fm,
|
||||
|
|
@ -18,8 +22,13 @@ export const Field: React.FC<any> = ({
|
|||
onChange,
|
||||
className,
|
||||
style,
|
||||
prefix,
|
||||
suffix,
|
||||
}) => {
|
||||
let result = null;
|
||||
|
||||
const suffixRef = useRef<HTMLDivElement | null>(null);
|
||||
const prefixRef = useRef<HTMLDivElement | null>(null);
|
||||
const is_disable = fm.mode === "view" ? true : disabled;
|
||||
const error = fm.error?.[name];
|
||||
useEffect(() => {
|
||||
|
|
@ -42,6 +51,8 @@ export const Field: React.FC<any> = ({
|
|||
fm.render();
|
||||
}
|
||||
}, []);
|
||||
const before = typeof prefix === "function" ? prefix() : prefix;
|
||||
const after = typeof suffix === "function" ? suffix() : suffix;
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -65,11 +76,28 @@ export const Field: React.FC<any> = ({
|
|||
<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" : ""
|
||||
? "flex flex-row rounded-md flex-grow border-red-500 border items-center"
|
||||
: "flex flex-row rounded-md flex-grow items-center",
|
||||
is_disable ? "bg-gray-100" : "",
|
||||
"relative"
|
||||
)}
|
||||
>
|
||||
{before && (
|
||||
<div
|
||||
ref={prefixRef}
|
||||
className={cx(
|
||||
"absolute left-[1px] px-1 py-1 bg-gray-200/50 border border-gray-100 items-center flex flex-row flex-grow rounded-l-md",
|
||||
css`
|
||||
height: 2.13rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`,
|
||||
is_disable ? "bg-gray-100" : "bg-gray-200/50"
|
||||
)}
|
||||
>
|
||||
{before}
|
||||
</div>
|
||||
)}
|
||||
{["upload"].includes(type) ? (
|
||||
<>
|
||||
<TypeUpload
|
||||
|
|
@ -139,6 +167,24 @@ export const Field: React.FC<any> = ({
|
|||
mode="single"
|
||||
/>
|
||||
</>
|
||||
) : ["richtext"].includes(type) ? (
|
||||
<>
|
||||
<TypeRichText
|
||||
fm={fm}
|
||||
name={name}
|
||||
disabled={is_disable}
|
||||
className={className}
|
||||
/>
|
||||
</>
|
||||
) : ["tag"].includes(type) ? (
|
||||
<>
|
||||
<TypeTag
|
||||
fm={fm}
|
||||
name={name}
|
||||
disabled={is_disable}
|
||||
className={className}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TypeInput
|
||||
|
|
@ -149,9 +195,41 @@ export const Field: React.FC<any> = ({
|
|||
type={type}
|
||||
disabled={is_disable}
|
||||
onChange={onChange}
|
||||
className={cx(
|
||||
before &&
|
||||
css`
|
||||
padding-left: ${getNumber(
|
||||
get(prefixRef, "current.clientWidth")
|
||||
) + 10}px;
|
||||
`,
|
||||
after &&
|
||||
css`
|
||||
padding-right: ${getNumber(
|
||||
get(suffixRef, "current.clientWidth")
|
||||
) + 10}px;
|
||||
`
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{after && (
|
||||
<div
|
||||
ref={suffixRef}
|
||||
className={cx(
|
||||
"absolute right-[1px] px-1 py-1 items-center flex flex-row flex-grow rounded-r-md",
|
||||
css`
|
||||
height: 2.13rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`,
|
||||
is_disable
|
||||
? "bg-gray-200/50 border-l border-gray-300"
|
||||
: "bg-gray-200/50 border border-gray-100"
|
||||
)}
|
||||
>
|
||||
{after}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="text-sm text-red-500 py-1">{error}</div>
|
||||
|
|
|
|||
|
|
@ -19,14 +19,16 @@ export const FormBetter: React.FC<any> = ({
|
|||
});
|
||||
useEffect(() => {}, [fm.data]);
|
||||
return (
|
||||
<div className="flex flex-col flex-grow">
|
||||
<div className="flex flex-col flex-grow gap-y-3">
|
||||
{typeof fm === "object" && typeof onTitle === "function" ? (
|
||||
<div className="flex flex-row w-full">{onTitle(fm)}</div>
|
||||
<div className="flex flex-row p-3 items-center bg-white border border-gray-300 rounded-lg">
|
||||
{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">
|
||||
<div className="w-full flex flex-row flex-grow bg-white rounded-lg border border-gray-300 relative overflow-y-scroll">
|
||||
<Form
|
||||
{...{
|
||||
children,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import Datepicker from "../../ui/Datepicker";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Textarea } from "../../ui/text-area";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import tinycolor from "tinycolor2";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
|
||||
export const TypeColor: React.FC<any> = ({
|
||||
value,
|
||||
onChangePicker,
|
||||
onClose,
|
||||
}) => {
|
||||
const meta = useLocal({
|
||||
originalValue: "",
|
||||
inputValue: value,
|
||||
rgbValue: "",
|
||||
selectedEd: "" as string,
|
||||
});
|
||||
useEffect(() => {
|
||||
meta.inputValue = value || "";
|
||||
const convertColor = tinycolor(meta.inputValue);
|
||||
meta.rgbValue = convertColor.toRgbString();
|
||||
meta.render();
|
||||
}, [value]);
|
||||
const tin = tinycolor(meta.inputValue);
|
||||
return (
|
||||
<div className="flex p-3 space-x-4 items-start">
|
||||
<div
|
||||
className={cx(
|
||||
"flex flex-col items-center",
|
||||
css`
|
||||
.react-colorful__pointer {
|
||||
border-radius: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
`
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Suspense>
|
||||
<HexColorPicker
|
||||
color={meta.inputValue}
|
||||
onChange={(color) => {
|
||||
if (color) {
|
||||
meta.inputValue = color;
|
||||
onChangePicker(color);
|
||||
const convertColor = tinycolor(meta.inputValue);
|
||||
meta.rgbValue = convertColor.toRgbString();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
"grid grid-cols-1 gap-y-0.5",
|
||||
css`
|
||||
width: 78px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="p-[1px] border rounded flex items-center justify-center"
|
||||
style={{
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
value={meta.inputValue || "#FFFFFFFF"}
|
||||
className={cx(
|
||||
`rounded cursor-text bg-[${meta.inputValue}] min-w-[0px] text-[13px] px-[8px] py-[1px] uppercase`,
|
||||
tin.isValid() &&
|
||||
css`
|
||||
color: ${!tin.isLight() ? "#FFF" : "#000"};
|
||||
background-color: ${meta.inputValue || ""};
|
||||
`
|
||||
)}
|
||||
spellCheck={false}
|
||||
onChange={(e) => {
|
||||
const color = e.currentTarget.value;
|
||||
meta.inputValue = color;
|
||||
onChangePicker(color);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
{meta.inputValue !== "" && (
|
||||
<>
|
||||
<div
|
||||
className="cursor-pointer text-center border border-gray-200 rounded hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
meta.inputValue = "";
|
||||
onChangePicker("");
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{onClose && (
|
||||
<div
|
||||
className="cursor-pointer text-center border border-gray-200 rounded hover:bg-gray-100 mt-[4px]"
|
||||
onClick={onClose}
|
||||
>
|
||||
Close
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -3,6 +3,8 @@ import Datepicker from "../../ui/Datepicker";
|
|||
import { Input } from "../../ui/input";
|
||||
import { Textarea } from "../../ui/text-area";
|
||||
import { useEffect } from "react";
|
||||
import tinycolor from "tinycolor2";
|
||||
import { FieldColorPicker } from "../../ui/FieldColorPopover";
|
||||
|
||||
export const TypeInput: React.FC<any> = ({
|
||||
name,
|
||||
|
|
@ -13,12 +15,28 @@ export const TypeInput: React.FC<any> = ({
|
|||
type,
|
||||
field,
|
||||
onChange,
|
||||
className,
|
||||
}) => {
|
||||
let value: any = fm.data?.[name] || "";
|
||||
const input = useLocal({
|
||||
value: 0 as any,
|
||||
ref: null as any,
|
||||
open: false,
|
||||
});
|
||||
const meta = useLocal({
|
||||
originalValue: "",
|
||||
inputValue: value,
|
||||
rgbValue: "",
|
||||
selectedEd: "" as string,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (type === "color") {
|
||||
meta.inputValue = value || "";
|
||||
const convertColor = tinycolor(meta.inputValue);
|
||||
meta.rgbValue = convertColor.toRgbString();
|
||||
meta.render();
|
||||
}
|
||||
}, [value]);
|
||||
useEffect(() => {
|
||||
if (type === "money") {
|
||||
input.value =
|
||||
|
|
@ -67,6 +85,51 @@ export const TypeInput: React.FC<any> = ({
|
|||
);
|
||||
break;
|
||||
|
||||
case "color":
|
||||
return (
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="border border-gray-300 p-0.5 rounded-sm">
|
||||
<FieldColorPicker
|
||||
value={fm.data?.[name]}
|
||||
update={(val) => {
|
||||
fm.data[name] = val;
|
||||
fm.render();
|
||||
}}
|
||||
onOpen={() => {
|
||||
input.open = true;
|
||||
input.render();
|
||||
}}
|
||||
onClose={() => {
|
||||
input.open = false;
|
||||
input.render();
|
||||
}}
|
||||
open={input.open}
|
||||
showHistory={false}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
css`
|
||||
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill-opacity=".05"><path d="M8 0h8v8H8zM0 8h8v8H0z"/></svg>');
|
||||
`,
|
||||
"cursor-pointer rounded-md"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
"rounded-sm h-8 w-8",
|
||||
css`
|
||||
background: ${fm?.data?.[name]};
|
||||
`,
|
||||
"color-box"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</FieldColorPicker>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
|
||||
case "date":
|
||||
return (
|
||||
<>
|
||||
|
|
@ -111,7 +174,8 @@ export const TypeInput: React.FC<any> = ({
|
|||
? "rgb(243 244 246)"
|
||||
: "transparant"}
|
||||
? "";
|
||||
`
|
||||
`,
|
||||
className
|
||||
)}
|
||||
required={required}
|
||||
placeholder={placeholder || ""}
|
||||
|
|
@ -165,7 +229,8 @@ export const TypeInput: React.FC<any> = ({
|
|||
css`
|
||||
background-color: ${disabled ? "rgb(243 244 246)" : "transparant"} ?
|
||||
"";
|
||||
`
|
||||
`,
|
||||
className
|
||||
)}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,821 @@
|
|||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { Input } from "../../ui/input";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
useEditor,
|
||||
EditorContent,
|
||||
useCurrentEditor,
|
||||
EditorProvider,
|
||||
} from "@tiptap/react";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import ListItem from "@tiptap/extension-list-item";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import { Popover } from "../../Popover/Popover";
|
||||
import { ButtonBetter } from "../../ui/button";
|
||||
import get from "lodash.get";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/tabs";
|
||||
import Table from "@tiptap/extension-table";
|
||||
import TableCell from "@tiptap/extension-table-cell";
|
||||
import TableHeader from "@tiptap/extension-table-header";
|
||||
import TableRow from "@tiptap/extension-table-row";
|
||||
import { ButtonRichText } from "../../ui/button-rich-text";
|
||||
|
||||
export const TypeRichText: 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,
|
||||
open: false,
|
||||
});
|
||||
const [url, setUrl] = useState(null as any);
|
||||
const local = useLocal({
|
||||
open: false,
|
||||
data: ["General", "Table"],
|
||||
tab: 0,
|
||||
active: "General",
|
||||
});
|
||||
useEffect(() => {}, [fm.data?.[name]]);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
const MenuBar = () => {
|
||||
const { editor } = useCurrentEditor();
|
||||
if (disabled) return <></>;
|
||||
if (!editor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"control-group sticky top-0 bg-white shadow rounded-t-lg",
|
||||
css`
|
||||
z-index: 2;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<Tabs
|
||||
className="flex flex-col w-full pt-1 bg-gray-100 rounded-t-lg"
|
||||
defaultValue={local.active}
|
||||
value={local.active}
|
||||
>
|
||||
<TabsList className="flex flex-row relative w-full p-0 rounded-none rounded-t-md justify-start">
|
||||
{local.data.map((e, idx) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center relative"
|
||||
key={`container_tab_${e}_${idx}`}
|
||||
>
|
||||
<TabsTrigger
|
||||
value={e}
|
||||
onClick={() => {
|
||||
local.tab = idx;
|
||||
local.active = e;
|
||||
local.render();
|
||||
}}
|
||||
className={cx(
|
||||
"p-1.5 px-4 border text-sm font-normal data-[state=active]:font-bold data-[state=active]:bg-white border-none border-r bg-transparent data-[state=active]:text-primary data-[state=active]:shadow-none data-[state=active]:border-none",
|
||||
!idx
|
||||
? "ml-1.5"
|
||||
: idx++ === local.data.length
|
||||
? "mr-2"
|
||||
: ""
|
||||
)}
|
||||
key={e}
|
||||
>
|
||||
{e}
|
||||
</TabsTrigger>
|
||||
<div
|
||||
className={cx(
|
||||
"w-0.5 h-4 bg-white rounded-full absolute right-[-1px]",
|
||||
css`
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
`,
|
||||
(idx === local.tab || idx + 1 === local.tab) && "hidden"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
<TabsContent value={"General"} className="bg-gray-100">
|
||||
<div className="button-group flex flex-row gap-x-2 p-2 rounded-t-lg bg-white">
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
disabled={!editor.can().chain().focus().toggleBold().run()}
|
||||
className={cx(
|
||||
editor.isActive("bold") ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md px-2"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 2h4.5a3.501 3.501 0 0 1 2.852 5.53A3.499 3.499 0 0 1 9.5 14H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1m1 7v3h4.5a1.5 1.5 0 0 0 0-3Zm3.5-2a1.5 1.5 0 0 0 0-3H5v3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
disabled={!editor.can().chain().focus().toggleItalic().run()}
|
||||
className={cx(
|
||||
editor.isActive("italic") ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6h4m4 0h-4m0 0l-4 12m0 0h4m-4 0H6"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
className={cx(
|
||||
editor.isActive("underline") ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 5v5a5 5 0 0 0 10 0V5M5 19h14"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
disabled={!editor.can().chain().focus().toggleStrike().run()}
|
||||
className={cx(
|
||||
editor.isActive("strike") ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.154 14q.346.774.346 1.72q0 2.014-1.571 3.147Q14.357 20 11.586 20q-2.46 0-4.87-1.145v-2.254q2.28 1.316 4.666 1.316q3.826 0 3.839-2.197a2.2 2.2 0 0 0-.648-1.603l-.12-.117H3v-2h18v2zm-4.078-3H7.629a4 4 0 0 1-.481-.522Q6.5 9.643 6.5 8.452q0-1.854 1.397-3.153T12.222 4q2.207 0 4.222.984v2.152q-1.8-1.03-3.946-1.03q-3.72 0-3.719 2.346q0 .63.654 1.099q.654.47 1.613.75q.93.27 2.03.699"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
className={cx(
|
||||
editor.isActive("bulletList") ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 15 15"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M1.5 5.25a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M4 4.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5M4.5 7a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1zM2.25 7.5a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m-.75 3.75a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
className={cx(
|
||||
editor.isActive("orderedList") ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g fill="none">
|
||||
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5.436 16.72a1.466 1.466 0 0 1 1.22 2.275a1.466 1.466 0 0 1-1.22 2.275c-.65 0-1.163-.278-1.427-.901a.65.65 0 1 1 1.196-.508a.18.18 0 0 0 .165.109c.109 0 .23-.03.23-.167c0-.1-.073-.143-.156-.154l-.051-.004a.65.65 0 0 1-.096-1.293l.096-.007c.102 0 .207-.037.207-.158c0-.137-.12-.167-.23-.167a.18.18 0 0 0-.164.11a.65.65 0 1 1-1.197-.509c.264-.622.777-.9 1.427-.9ZM20 18a1 1 0 1 1 0 2H9a1 1 0 1 1 0-2zM6.08 9.945a1.552 1.552 0 0 1 .43 2.442l-.554.593h.47a.65.65 0 1 1 0 1.3H4.573a.655.655 0 0 1-.655-.654c0-.207.029-.399.177-.557L5.559 11.5c.11-.117.082-.321-.06-.392c-.136-.068-.249.01-.275.142l-.006.059a.65.65 0 0 1-.65.65c-.39 0-.65-.327-.65-.697a1.482 1.482 0 0 1 2.163-1.317ZM20 11a1 1 0 0 1 .117 1.993L20 13H9a1 1 0 0 1-.117-1.993L9 11zM6.15 3.39v3.24a.65.65 0 1 1-1.3 0V4.522a.65.65 0 0 1-.46-1.183l.742-.495a.655.655 0 0 1 1.018.545ZM20 4a1 1 0 0 1 .117 1.993L20 6H9a1 1 0 0 1-.117-1.993L9 4z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().setTextAlign("left").run()
|
||||
}
|
||||
className={cx(
|
||||
editor.isActive({ textAlign: "left" })
|
||||
? "is-active bg-gray-200"
|
||||
: "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h10M4 18h14"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().setTextAlign("center").run()
|
||||
}
|
||||
className={cx(
|
||||
editor.isActive({ textAlign: "center" })
|
||||
? "is-active bg-gray-200"
|
||||
: "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M8 12h8M6 18h12"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().setTextAlign("right").run()
|
||||
}
|
||||
className={cx(
|
||||
editor.isActive({ textAlign: "right" })
|
||||
? "is-active bg-gray-200"
|
||||
: "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16m-10 6h10M6 18h14"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().setTextAlign("justify").run()
|
||||
}
|
||||
className={cx(
|
||||
editor.isActive({ textAlign: "justify" })
|
||||
? "is-active bg-gray-200"
|
||||
: "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h12"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
}
|
||||
className={cx(
|
||||
editor.isActive("heading", { level: 1 })
|
||||
? "is-active bg-gray-200"
|
||||
: "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M14 4.25a.75.75 0 0 0-1.248-.56l-2.25 2a.75.75 0 0 0 .996 1.12l1.002-.89v5.83a.75.75 0 0 0 1.5 0zm-11.5 0a.75.75 0 0 0-1.5 0v7.496a.75.75 0 0 0 1.5 0V8.75h4v2.996a.75.75 0 0 0 1.5 0V4.25a.75.75 0 0 0-1.5 0v3h-4z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
}
|
||||
className={cx(
|
||||
editor.isActive("heading", { level: 2 })
|
||||
? "is-active bg-gray-200"
|
||||
: "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M2.5 4.25a.75.75 0 0 0-1.5 0v7.496a.75.75 0 0 0 1.5 0V8.75h4v2.996a.75.75 0 0 0 1.5 0V4.25a.75.75 0 0 0-1.5 0v3h-4zm8.403 1.783A1.364 1.364 0 0 1 12.226 5h.226a1.071 1.071 0 0 1 .672 1.906l-3.61 2.906a1.51 1.51 0 0 0 .947 2.688h3.789a.75.75 0 0 0 0-1.5h-3.793l-.003-.003l-.003-.004v-.004a.01.01 0 0 1 .004-.008l3.61-2.907A2.571 2.571 0 0 0 12.452 3.5h-.226c-1.314 0-2.46.894-2.778 2.17l-.038.148a.75.75 0 1 0 1.456.364z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||
}
|
||||
className={cx(
|
||||
editor.isActive("heading", { level: 3 })
|
||||
? "is-active bg-gray-200"
|
||||
: "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M2.5 4.25a.75.75 0 0 0-1.5 0v7.496a.75.75 0 0 0 1.5 0V8.75h4v2.996a.75.75 0 0 0 1.5 0V4.25a.75.75 0 0 0-1.5 0v3h-4zm8.114 1.496c.202-.504.69-.834 1.232-.834h.28a.94.94 0 0 1 .929.796l.027.18a1.15 1.15 0 0 1-.911 1.303l-.8.16a.662.662 0 0 0 .129 1.31h1.21a.89.89 0 0 1 .882 1.017a1.67 1.67 0 0 1-1.414 1.414l-.103.015a1.81 1.81 0 0 1-1.828-.9l-.018-.033a.662.662 0 0 0-1.152.652l.018.032a3.13 3.13 0 0 0 3.167 1.559l.103-.015a2.99 2.99 0 0 0 2.537-2.537a2.21 2.21 0 0 0-1.058-2.216a2.47 2.47 0 0 0 .547-1.963l-.028-.179a2.26 2.26 0 0 0-2.237-1.919h-.28a2.65 2.65 0 0 0-2.46 1.666a.662.662 0 1 0 1.228.492"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<Popover
|
||||
classNameTrigger={""}
|
||||
arrow={false}
|
||||
className="rounded-md"
|
||||
onOpenChange={(open: any) => {
|
||||
if (!editor.isActive("link")) {
|
||||
local.open = open;
|
||||
local.render();
|
||||
}
|
||||
}}
|
||||
open={local.open}
|
||||
content={
|
||||
<div className="flex flex-row px-2 py-4 gap-y-2 items-center">
|
||||
<Input
|
||||
id="maxWidth"
|
||||
value={url || ""}
|
||||
className="col-span-2 h-9"
|
||||
onChange={(e) => {
|
||||
setUrl(get(e, "currentTarget.value"));
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-row justify-end">
|
||||
<ButtonBetter
|
||||
onClick={() => {
|
||||
if (url) {
|
||||
try {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange("link")
|
||||
.setLink({ href: url })
|
||||
.run();
|
||||
local.open = false;
|
||||
local.render();
|
||||
setUrl("");
|
||||
} catch (e: any) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</ButtonBetter>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.chain().focus().unsetLink().run();
|
||||
}}
|
||||
className={cx(
|
||||
editor.isActive("link") ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md"
|
||||
)}
|
||||
>
|
||||
{editor.isActive("link") ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m15.221 12.41l-.91-.91h.65q.214 0 .357.143t.144.357q0 .135-.062.239q-.061.103-.179.17m5.625 9.145q-.16.16-.354.16t-.353-.16L2.445 3.862q-.14-.14-.15-.345t.15-.363t.354-.16t.354.16l17.692 17.692q.14.14.15.345q.01.203-.15.363M7.077 16.077q-1.69 0-2.884-1.193T3 12q0-1.61 1.098-2.777t2.69-1.265h.462l.966.965H7.077q-1.27 0-2.173.904Q4 10.731 4 12t.904 2.173t2.173.904h3.077q.213 0 .357.143t.143.357t-.143.357t-.357.143zM9.039 12.5q-.214 0-.357-.143T8.539 12t.143-.357t.357-.143h1.759l.975 1zm9.134 2.916q-.11-.178-.057-.385t.25-.298q.748-.387 1.19-1.118Q20 12.885 20 12q0-1.27-.894-2.173q-.895-.904-2.145-.904h-3.115q-.213 0-.356-.143t-.144-.357t.144-.357t.356-.143h3.116q1.67 0 2.854 1.193T21 12q0 1.148-.591 2.095q-.592.947-1.553 1.493q-.177.11-.375.057t-.308-.23"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.077 16.077q-1.692 0-2.884-1.192T3 12t1.193-2.885t2.884-1.193h3.039q.212 0 .356.144t.144.357t-.144.356t-.356.143H7.075q-1.267 0-2.171.904T4 12t.904 2.173t2.17.904h3.042q.212 0 .356.144t.144.357t-.144.356t-.356.143zM9 12.5q-.213 0-.356-.144t-.144-.357t.144-.356T9 11.5h6q.213 0 .356.144t.144.357t-.144.356T15 12.5zm4.885 3.577q-.213 0-.357-.144t-.144-.357t.144-.356t.356-.143h3.041q1.267 0 2.171-.904T20 12t-.904-2.173t-2.17-.904h-3.041q-.213 0-.357-.144q-.143-.144-.143-.357t.143-.356t.357-.143h3.038q1.692 0 2.885 1.192T21 12t-1.193 2.885t-2.884 1.193z"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</Popover>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"Table"} className="bg-gray-100">
|
||||
<div className="button-group flex flex-row gap-x-2 p-2 rounded-t-lg bg-white">
|
||||
<ButtonRichText
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||
.run();
|
||||
}}
|
||||
disabled={false}
|
||||
active={false}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M9 3.5v17m11.5-11h-17M3 9.4c0-2.24 0-3.36.436-4.216a4 4 0 0 1 1.748-1.748C6.04 3 7.16 3 9.4 3h5.2c2.24 0 3.36 0 4.216.436a4 4 0 0 1 1.748 1.748C21 6.04 21 7.16 21 9.4v5.2c0 2.24 0 3.36-.436 4.216a4 4 0 0 1-1.748 1.748C17.96 21 16.84 21 14.6 21H9.4c-2.24 0-3.36 0-4.216-.436a4 4 0 0 1-1.748-1.748C3 17.96 3 16.84 3 14.6z"
|
||||
></path>
|
||||
</svg>
|
||||
</ButtonRichText>
|
||||
<ButtonRichText
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
editor.commands.setCellAttribute(
|
||||
"className",
|
||||
"tiptap-border-none"
|
||||
);
|
||||
}}
|
||||
disabled={false}
|
||||
active={false}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 9q-.425 0-.712-.288T11 8t.288-.712T12 7t.713.288T13 8t-.288.713T12 9m-4 4q-.425 0-.712-.288T7 12t.288-.712T8 11t.713.288T9 12t-.288.713T8 13m4 0q-.425 0-.712-.288T11 12t.288-.712T12 11t.713.288T13 12t-.288.713T12 13m4 0q-.425 0-.712-.288T15 12t.288-.712T16 11t.713.288T17 12t-.288.713T16 13m-4 4q-.425 0-.712-.288T11 16t.288-.712T12 15t.713.288T13 16t-.288.713T12 17M4 5q-.425 0-.712-.288T3 4t.288-.712T4 3t.713.288T5 4t-.288.713T4 5m4 0q-.425 0-.712-.288T7 4t.288-.712T8 3t.713.288T9 4t-.288.713T8 5m4 0q-.425 0-.712-.288T11 4t.288-.712T12 3t.713.288T13 4t-.288.713T12 5m4 0q-.425 0-.712-.288T15 4t.288-.712T16 3t.713.288T17 4t-.288.713T16 5m4 0q-.425 0-.712-.288T19 4t.288-.712T20 3t.713.288T21 4t-.288.713T20 5M4 9q-.425 0-.712-.288T3 8t.288-.712T4 7t.713.288T5 8t-.288.713T4 9m16 0q-.425 0-.712-.288T19 8t.288-.712T20 7t.713.288T21 8t-.288.713T20 9M4 13q-.425 0-.712-.288T3 12t.288-.712T4 11t.713.288T5 12t-.288.713T4 13m16 0q-.425 0-.712-.288T19 12t.288-.712T20 11t.713.288T21 12t-.288.713T20 13M4 17q-.425 0-.712-.288T3 16t.288-.712T4 15t.713.288T5 16t-.288.713T4 17m16 0q-.425 0-.712-.288T19 16t.288-.712T20 15t.713.288T21 16t-.288.713T20 17M4 21q-.425 0-.712-.288T3 20t.288-.712T4 19t.713.288T5 20t-.288.713T4 21m4 0q-.425 0-.712-.288T7 20t.288-.712T8 19t.713.288T9 20t-.288.713T8 21m4 0q-.425 0-.712-.288T11 20t.288-.712T12 19t.713.288T13 20t-.288.713T12 21m4 0q-.425 0-.712-.288T15 20t.288-.712T16 19t.713.288T17 20t-.288.713T16 21m4 0q-.425 0-.712-.288T19 20t.288-.712T20 19t.713.288T21 20t-.288.713T20 21"
|
||||
></path>
|
||||
</svg>
|
||||
</ButtonRichText>
|
||||
|
||||
<ButtonRichText
|
||||
onClick={(e: any) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
editor.commands.setCellAttribute("className", "");
|
||||
}}
|
||||
disabled={false}
|
||||
active={false}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={25}
|
||||
height={25}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 5v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2m8 14H6c-.55 0-1-.45-1-1v-5h5c.55 0 1 .45 1 1zm-1-8H5V6c0-.55.45-1 1-1h5v5c0 .55-.45 1-1 1m8 8h-5v-5c0-.55.45-1 1-1h5v5c0 .55-.45 1-1 1m1-8h-5c-.55 0-1-.45-1-1V5h5c.55 0 1 .45 1 1z"
|
||||
></path>
|
||||
</svg>
|
||||
</ButtonRichText>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const CustomTable = Table.extend({
|
||||
resizable: true,
|
||||
addAttributes() {
|
||||
return {
|
||||
class: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const extensions = [
|
||||
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
HTMLAttributes: {
|
||||
class: "my-custom-class", // Tambahkan kelas default
|
||||
},
|
||||
}),
|
||||
TableRow,
|
||||
TableHeader.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
className: {
|
||||
default: null, // Nilai default tidak ada
|
||||
parseHTML: (element) => element.getAttribute("class") || null, // Ambil class dari elemen
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.className) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
class: attributes.className, // Tambahkan class ke HTML output
|
||||
};
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
default: null, // Nilai default
|
||||
parseHTML: (element) => element.style.backgroundColor || null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.backgroundColor) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
style: `background-color: ${attributes.backgroundColor};`,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
TableCell.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
className: {
|
||||
default: null, // Nilai default tidak ada
|
||||
parseHTML: (element) => element.getAttribute("class") || null, // Ambil class dari elemen
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.className) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
class: attributes.className, // Tambahkan class ke HTML output
|
||||
};
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
default: null, // Nilai default
|
||||
parseHTML: (element) => element.style.backgroundColor || null,
|
||||
renderHTML: (attributes) => {
|
||||
if (!attributes.backgroundColor) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
style: `background-color: ${attributes.backgroundColor};`,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ["heading", "paragraph"],
|
||||
}),
|
||||
Underline,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
autolink: true,
|
||||
defaultProtocol: "https",
|
||||
protocols: ["http", "https"],
|
||||
isAllowedUri: (url: any, ctx: any) => {
|
||||
try {
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(":")
|
||||
? new URL(url)
|
||||
: new URL(`${ctx.defaultProtocol}://${url}`);
|
||||
|
||||
// use default validation
|
||||
if (!ctx.defaultValidate(parsedUrl.href)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallowed protocols
|
||||
const disallowedProtocols = ["ftp", "file", "mailto"];
|
||||
const protocol = parsedUrl.protocol.replace(":", "");
|
||||
|
||||
if (disallowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only allow protocols specified in ctx.protocols
|
||||
const allowedProtocols = ctx.protocols.map((p: any) =>
|
||||
typeof p === "string" ? p : p.scheme
|
||||
);
|
||||
|
||||
if (!allowedProtocols.includes(protocol)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallowed domains
|
||||
const disallowedDomains = [
|
||||
"example-phishing.com",
|
||||
"malicious-site.net",
|
||||
];
|
||||
const domain = parsedUrl.hostname;
|
||||
|
||||
if (disallowedDomains.includes(domain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// all checks have passed
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
shouldAutoLink: (url: any) => {
|
||||
try {
|
||||
// construct URL
|
||||
const parsedUrl = url.includes(":")
|
||||
? new URL(url)
|
||||
: new URL(`https://${url}`);
|
||||
|
||||
// only auto-link if the domain is not in the disallowed list
|
||||
const disallowedDomains = [
|
||||
"example-no-autolink.com",
|
||||
"another-no-autolink.com",
|
||||
];
|
||||
const domain = parsedUrl.hostname;
|
||||
|
||||
return !disallowedDomains.includes(domain);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
}),
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
orderedList: {
|
||||
keepMarks: true,
|
||||
keepAttributes: false, // TODO : Making this as `false` becase marks are not preserved when I try to preserve attrs, awaiting a bit of help
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
"flex flex-col relative bg-white border border-gray-300 rounded-md w-full",
|
||||
css`
|
||||
.tiptap h1 {
|
||||
font-size: 1.4rem !important;
|
||||
}
|
||||
|
||||
.tiptap h2 {
|
||||
font-size: 1.2rem !important;
|
||||
}
|
||||
|
||||
.tiptap h3 {
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
.ProseMirror {
|
||||
outline: none !important;
|
||||
padding: 10px 2rem 10px 2rem;
|
||||
}
|
||||
.tiptap a {
|
||||
font-weight: bold;
|
||||
color: #313678;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.ProseMirror ul,
|
||||
ol {
|
||||
padding: 0 1rem;
|
||||
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
||||
}
|
||||
.ProseMirror ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
.ProseMirror ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
<EditorProvider
|
||||
slotBefore={<MenuBar />}
|
||||
extensions={extensions}
|
||||
onUpdate={({ editor }) => {
|
||||
fm.data[name] = editor.getHTML();
|
||||
fm.render();
|
||||
}}
|
||||
content={fm.data[name]}
|
||||
editable={!disabled}
|
||||
></EditorProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { Input } from "../../ui/input";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
useEditor,
|
||||
EditorContent,
|
||||
useCurrentEditor,
|
||||
EditorProvider,
|
||||
} from "@tiptap/react";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
import ListItem from "@tiptap/extension-list-item";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import { Popover } from "../../Popover/Popover";
|
||||
import { ButtonBetter } from "../../ui/button";
|
||||
import get from "lodash.get";
|
||||
|
||||
export const TypeTag: React.FC<any> = ({
|
||||
name,
|
||||
fm,
|
||||
placeholder,
|
||||
disabled = false,
|
||||
required,
|
||||
type,
|
||||
field,
|
||||
onChange,
|
||||
}) => {
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [editingIndex, setEditingIndex] = useState<number | null>(null); // Index tag yang sedang diedit
|
||||
const [tempValue, setTempValue] = useState<string>(""); // Nilai sementara untuk pengeditan
|
||||
const tagRefs = useRef<(HTMLDivElement | null)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (get(fm, `data.[${name}].length`)) {
|
||||
setTags(fm.data?.[name]);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
fm.data[name] = tags;
|
||||
fm.render();
|
||||
}, [inputValue]);
|
||||
const handleSaveEdit = (index: number) => {
|
||||
if (!disabled) return;
|
||||
const updatedTags = [...tags];
|
||||
updatedTags[index] = tempValue.trim(); // Update nilai tag
|
||||
setTags(updatedTags);
|
||||
setEditingIndex(null); // Keluar dari mode edit
|
||||
setTempValue(""); // Reset nilai sementara
|
||||
};
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (!disabled) return;
|
||||
if (e.key === "Enter" && inputValue) {
|
||||
e.preventDefault();
|
||||
setTags([...tags, inputValue]);
|
||||
setInputValue("");
|
||||
} else if (e.key === "Backspace" && !inputValue && tags.length > 0) {
|
||||
setTags(tags.slice(0, -1));
|
||||
}
|
||||
};
|
||||
const handleFocusTag = (index: number) => {
|
||||
if (!disabled) return;
|
||||
setEditingIndex(index); // Masuk ke mode edit
|
||||
setTempValue(tags[index]); // Isi nilai sementara dengan nilai tag
|
||||
setTimeout(() => {
|
||||
tagRefs.current[index]?.focus(); // Fokus pada elemen yang diedit
|
||||
}, 0);
|
||||
};
|
||||
const removeTag = (index: number) => {
|
||||
if (!disabled) return;
|
||||
setTags(tags.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center border border-gray-300 rounded-md flex-grow ">
|
||||
{tags.map((tag, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-row items-center bg-blue-100 text-blue-800 rounded-full m-1 text-sm"
|
||||
>
|
||||
{disabled ? (
|
||||
<div className="px-2">{tag}</div>
|
||||
) : (
|
||||
<div
|
||||
className={cx(
|
||||
"px-3 py-1 pr-0 flex-grow focus:shadow-none focus:ring-0 focus:border-none focus:outline-none",
|
||||
editingIndex! !== index && "cursor-pointer"
|
||||
)}
|
||||
contentEditable={editingIndex === index}
|
||||
suppressContentEditableWarning
|
||||
onBlur={() => handleSaveEdit(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (!disabled) return;
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleSaveEdit(index);
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
setEditingIndex(null);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
handleFocusTag(index);
|
||||
}}
|
||||
onInput={(e) =>
|
||||
setTempValue((e.target as HTMLDivElement).innerText)
|
||||
}
|
||||
>
|
||||
{tag}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(index)}
|
||||
className="ml-2 text-blue-500 hover:text-blue-700 pr-2"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!disabled && (
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="rounded-md flex-grow border-none outline-none text-sm focus:shadow-none focus:ring-0 focus:border-none focus:outline-none"
|
||||
placeholder="Add a option..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -446,6 +446,10 @@ export const Typeahead: FC<{
|
|||
}
|
||||
local.open = open;
|
||||
local.render();
|
||||
|
||||
if (!open) {
|
||||
resetSearch();
|
||||
}
|
||||
}}
|
||||
showEmpty={!allow_new}
|
||||
className={popupClassName}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
"use client";
|
||||
import React, { FC } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
DarkThemeToggle,
|
||||
Dropdown,
|
||||
Label,
|
||||
Navbar,
|
||||
TextInput,
|
||||
} from "flowbite-react";
|
||||
import { Avatar, Dropdown, Navbar } from "flowbite-react";
|
||||
import {
|
||||
HiArchive,
|
||||
HiBell,
|
||||
|
|
@ -16,50 +9,27 @@ import {
|
|||
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">
|
||||
<Navbar fluid className="bg-transparent pt-0 pr-6 pb-0">
|
||||
<div className="w-full p-1 lg:px-5 lg:pl-3 rounded rounded-lg">
|
||||
<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">
|
||||
<div className="flex items-center"></div>
|
||||
<div className="flex flex-row gap-x-3 justify-center ">
|
||||
<div className="flex flex-row items-center flex-grow">
|
||||
<NotificationBellDropdown />
|
||||
</div>
|
||||
<div className="hidden lg:block">
|
||||
|
||||
<div className="hidden lg:flex flex-row justify-center">
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -403,10 +373,16 @@ const UserDropdown: FC = function () {
|
|||
arrowIcon={false}
|
||||
inline
|
||||
label={
|
||||
<span>
|
||||
<span className="sr-only">User menu</span>
|
||||
<div className="flex flex-row justify-center">
|
||||
<div className="flex flex-row items-center flex-grow">
|
||||
<div className="border-l border-gray-200 px-2 h-full flex items-end justify-center flex-col text-xs max-w-[100px]">
|
||||
<div>
|
||||
{get_user("employee.name") ? get_user("employee.name") : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Avatar alt="" img={siteurl("/dog.jpg")} rounded size="sm" />
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Dropdown.Header>
|
||||
|
|
@ -430,8 +406,10 @@ const UserDropdown: FC = function () {
|
|||
<Dropdown.Divider />
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
await api.delete(process.env.NEXT_PUBLIC_BASE_URL + "/api/destroy-cookies");
|
||||
localStorage.removeItem('user');
|
||||
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`);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
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!);
|
||||
let isActive = item.href && detectCase(currentPage, item.href);
|
||||
let isParentActive = hasChildren && isChildActive(item.children!);
|
||||
const [isOpen, setIsOpen] = useState(isParentActive);
|
||||
useEffect(() => {
|
||||
if (isParentActive) {
|
||||
|
|
@ -71,14 +71,41 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
return (
|
||||
<React.Fragment key={item.href || item.title || index}>
|
||||
{hasChildren ? (
|
||||
<li>
|
||||
<li className="relative">
|
||||
{mini && isParentActive && (
|
||||
<div
|
||||
className={cx("absolute top-[-15px] right-[-1px] text-layer")}
|
||||
>
|
||||
<svg
|
||||
width="184"
|
||||
height="167"
|
||||
viewBox="0 0 184 167"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17 167C109.232 167 184 92.2316 184 0V167H17ZM0 166.145C5.58984 166.711 11.2611 167 17 167H0V166.145Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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 ",
|
||||
"relative flex-row flex items-center cursor-pointer items-center w-full rounded-full rounded-r-none text-base font-normal text-gray-900 flex flex-row",
|
||||
|
||||
mini
|
||||
? isParentActive && !depth
|
||||
? " text-base font-normal text-primary rounded-full rounded-r-none group bg-layer transition-all duration-200 dark:bg-gray-700"
|
||||
: " text-white"
|
||||
: isActive && !depth
|
||||
? " text-base font-normal text-primary rounded-full rounded-r-none group bg-layer transition-all duration-200 dark:bg-gray-700"
|
||||
: " text-white",
|
||||
mini ? "pr-4 m-0 flex-grow w-full" : "py-2.5 px-4 ",
|
||||
mini
|
||||
? css`
|
||||
margin: 0 !important;
|
||||
|
|
@ -96,24 +123,30 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
<div
|
||||
className={cx(
|
||||
"flex flex-row items-center flex-grow",
|
||||
mini ? "py-2 justify-center rounded-lg" : " px-3",
|
||||
mini
|
||||
? "py-2 justify-center rounded-full rounded-r-none"
|
||||
: " px-3",
|
||||
mini
|
||||
? isParentActive
|
||||
? "bg-[#313678]"
|
||||
: "bg-white hover:bg-gray-300 shadow shadow-gray-300"
|
||||
: ""
|
||||
? "bg-layer font-bold "
|
||||
: "bg-primary text-white"
|
||||
: isActive
|
||||
? "font-bold text-white "
|
||||
: "text-white"
|
||||
)}
|
||||
>
|
||||
{!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",
|
||||
" w-8 h-8 text-center flex flex-row items-center justify-center",
|
||||
mini
|
||||
? isParentActive
|
||||
? "text-primary "
|
||||
: " text-white"
|
||||
: isActive
|
||||
? "text-primary "
|
||||
: " text-white",
|
||||
!mini ? "mr-1 p-2 " : " text-lg ",
|
||||
mini
|
||||
? css`
|
||||
background: transparent !important;
|
||||
|
|
@ -129,10 +162,10 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
|
||||
{!mini ? (
|
||||
<>
|
||||
<div className="pl-2 flex-grow text-black text-xs">
|
||||
<div className="pl-2 flex-grow text-xs">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="text-md">
|
||||
<div className="text-md px-1">
|
||||
{isOpen ? <FaChevronUp /> : <FaChevronDown />}
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -140,8 +173,27 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mini && isParentActive && (
|
||||
<div className=" absolute bottom-[-15px] right-[-1px] text-layer">
|
||||
<svg
|
||||
width="147"
|
||||
height="147"
|
||||
viewBox="0 0 147 147"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 0H147V147C147 65.8141 81.1859 0 0 0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Sidebar.ItemGroup
|
||||
className={classNames(
|
||||
"border-none mt-0",
|
||||
|
|
@ -153,22 +205,46 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
</Sidebar.ItemGroup>
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<li className="relative">
|
||||
{isActive && (
|
||||
<div
|
||||
className={cx(
|
||||
" absolute top-[-15px] right-[-1px] text-layer"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="184"
|
||||
height="167"
|
||||
viewBox="0 0 184 167"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M17 167C109.232 167 184 92.2316 184 0V167H17ZM0 166.145C5.58984 166.711 11.2611 167 17 167H0V166.145Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<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",
|
||||
"relative flex-row flex items-center cursor-pointer items-center w-full rounded-full rounded-r-none text-base text-gray-900 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"
|
||||
: "",
|
||||
? " py-2.5 px-4 text-base rounded-full rounded-r-none group "
|
||||
: " font-normal",
|
||||
mini ? "transition-all duration-200" : "",
|
||||
isActive
|
||||
? !depth
|
||||
? " bg-white shadow-md hover:bg-gray-200 hover:!bg-white "
|
||||
: "bg-gray-100"
|
||||
: "",
|
||||
? " bg-layer font-normal"
|
||||
: " bg-layer text-primary font-bold"
|
||||
: "text-white",
|
||||
css`
|
||||
& > span {
|
||||
white-space: wrap !important;
|
||||
|
|
@ -183,10 +259,8 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
{!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",
|
||||
" text-dark-700 w-8 h-8 rounded-lg text-center flex flex-row items-center justify-center ",
|
||||
isActive ? "bg-[#313678] " : "bg-layer text-white",
|
||||
!mini ? "mr-1 p-2" : " text-lg"
|
||||
)}
|
||||
>
|
||||
|
|
@ -197,14 +271,35 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
)}
|
||||
{!mini ? (
|
||||
<>
|
||||
<div className="pl-2 text-black text-xs">
|
||||
{item.title}
|
||||
</div>
|
||||
<div className="pl-2 text-xs">{item.title}</div>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
{isActive && (
|
||||
<div
|
||||
className={cx(
|
||||
"absolute bottom-[-15px] right-[-1px] text-layer"
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="147"
|
||||
height="147"
|
||||
viewBox="0 0 147 147"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M0 0H147V147C147 65.8141 81.1859 0 0 0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</SidebarLinkBetter>
|
||||
</li>
|
||||
)}
|
||||
|
|
@ -214,46 +309,27 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={classNames("flex h-full lg:!block", {})}>
|
||||
<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",
|
||||
className={classNames(
|
||||
"relative bg-primary pt-0 sidebar",
|
||||
mini ? "w-20" : "",
|
||||
css`
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
> div {
|
||||
background: transparent;
|
||||
padding-top: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
<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="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",
|
||||
"border-none mt-0 pt-4",
|
||||
mini ? "flex flex-col gap-y-2" : ""
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import React, { FC, useCallback, useEffect, useState } from "react";
|
|||
import {
|
||||
Breadcrumb,
|
||||
Button,
|
||||
Checkbox,
|
||||
Label,
|
||||
Modal,
|
||||
Table,
|
||||
|
|
@ -44,12 +43,15 @@ import { InputSearch } from "../ui/input-search";
|
|||
import { Input } from "../ui/input";
|
||||
import { FaChevronDown } from "react-icons/fa";
|
||||
import get from "lodash.get";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import { getNumber } from "@/lib/utils/getNumber";
|
||||
import { formatMoney } from "../form/field/TypeInput";
|
||||
|
||||
export const TableList: React.FC<any> = ({
|
||||
name,
|
||||
column,
|
||||
onLoad,
|
||||
take = 50,
|
||||
take = 20,
|
||||
header,
|
||||
disabledPagination,
|
||||
disabledHeader,
|
||||
|
|
@ -57,6 +59,8 @@ export const TableList: React.FC<any> = ({
|
|||
hiddenNoRow,
|
||||
disabledHoverRow,
|
||||
onInit,
|
||||
onCount,
|
||||
feature,
|
||||
}) => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const sideLeft =
|
||||
|
|
@ -71,11 +75,16 @@ export const TableList: React.FC<any> = ({
|
|||
status: string;
|
||||
progress: number;
|
||||
};
|
||||
const checkbox =
|
||||
Array.isArray(feature) && feature?.length
|
||||
? feature.includes("checkbox")
|
||||
: false;
|
||||
const local = useLocal({
|
||||
table: null as any,
|
||||
data: [] as any[],
|
||||
sort: {} as any,
|
||||
search: null as any,
|
||||
count: 0 as any,
|
||||
addRow: (row: any) => {
|
||||
setData((prev) => [...prev, row]);
|
||||
local.data.push(row);
|
||||
|
|
@ -145,9 +154,7 @@ export const TableList: React.FC<any> = ({
|
|||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
if (typeof onInit === "function") {
|
||||
onInit(local);
|
||||
}
|
||||
const run = async () => {
|
||||
toast.info(
|
||||
<>
|
||||
<Loader2
|
||||
|
|
@ -169,27 +176,23 @@ export const TableList: React.FC<any> = ({
|
|||
{"Loading..."}
|
||||
</>
|
||||
);
|
||||
if (typeof onCount === "function") {
|
||||
const res = await onCount();
|
||||
local.count = res;
|
||||
local.render();
|
||||
}
|
||||
|
||||
if (Array.isArray(onLoad)) {
|
||||
local.data = onLoad;
|
||||
local.render();
|
||||
setData(onLoad);
|
||||
} else {
|
||||
const res: any = onLoad({
|
||||
const res: any = await 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);
|
||||
|
|
@ -197,13 +200,53 @@ export const TableList: React.FC<any> = ({
|
|||
toast.dismiss();
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
if (typeof onInit === "function") {
|
||||
onInit(local);
|
||||
}
|
||||
run();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
// console.log("PERUBAHAN");
|
||||
}, [data]);
|
||||
const objectNull = {};
|
||||
const defaultColumns: ColumnDef<Person>[] = init_column(column);
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columns] = React.useState<typeof defaultColumns>(() => [
|
||||
const [columns] = React.useState<typeof defaultColumns>(() =>
|
||||
checkbox
|
||||
? [
|
||||
{
|
||||
id: "select",
|
||||
width: 10,
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
id="terms"
|
||||
checked={table.getIsAllRowsSelected()}
|
||||
onClick={(e) => {
|
||||
table.getToggleAllRowsSelectedHandler();
|
||||
const handler = table.getToggleAllRowsSelectedHandler();
|
||||
handler(e); // Pastikan ini memanggil fungsi handler yang benar
|
||||
}}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="px-0.5 items-center justify-center flex flex-row">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
checked={row.getIsSelected()}
|
||||
onClick={(e) => {
|
||||
const handler = row.getToggleSelectedHandler();
|
||||
handler(e); // Pastikan ini memanggil fungsi handler yang benar
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
sortable: false,
|
||||
},
|
||||
...defaultColumns,
|
||||
]);
|
||||
]
|
||||
: [...defaultColumns]
|
||||
);
|
||||
const [columnResizeMode, setColumnResizeMode] =
|
||||
React.useState<ColumnResizeMode>("onChange");
|
||||
|
||||
|
|
@ -218,25 +261,29 @@ export const TableList: React.FC<any> = ({
|
|||
: {
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
};
|
||||
const [pagination, setPagination] = React.useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 20,
|
||||
});
|
||||
const table = useReactTable({
|
||||
data: data,
|
||||
columnResizeMode,
|
||||
pageCount: Math.ceil(local.count / 20),
|
||||
manualPagination: true,
|
||||
columnResizeDirection,
|
||||
columns,
|
||||
enableRowSelection: true,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageIndex: 0, //custom initial page index
|
||||
pageSize: 25, //custom default page size
|
||||
pageIndex: 0,
|
||||
pageSize: 20, //custom default page size
|
||||
},
|
||||
},
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: 0,
|
||||
pageSize: 50,
|
||||
},
|
||||
pagination,
|
||||
sorting,
|
||||
},
|
||||
...paginationConfig,
|
||||
|
|
@ -263,19 +310,9 @@ export const TableList: React.FC<any> = ({
|
|||
<>
|
||||
<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="head-tbl-list block items-start justify-between bg-white px-0 py-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)
|
||||
|
|
@ -333,9 +370,35 @@ export const TableList: React.FC<any> = ({
|
|||
<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 ">
|
||||
<Table
|
||||
className={cx(
|
||||
"min-w-full divide-y divide-gray-200 ",
|
||||
css`
|
||||
thead th:first-child {
|
||||
overflow: hidden;
|
||||
border-top-left-radius: 10px; /* Sudut kiri atas */
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
thead th:last-child {
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 10px; /* Sudut kiri atas */
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
tbody td:first-child {
|
||||
overflow: hidden;
|
||||
border-top-left-radius: 10px; /* Sudut kiri atas */
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
tbody td:last-child {
|
||||
overflow: hidden;
|
||||
border-top-right-radius: 10px; /* Sudut kiri atas */
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
{!disabledHeadTable ? (
|
||||
<thead className="text-md bg-second group/head text-md uppercase text-gray-700 sticky top-0">
|
||||
<thead className="rounded-md overflow-hidden text-md bg-second group/head text-md uppercase text-gray-700 sticky top-0">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr
|
||||
key={`${headerGroup.id}`}
|
||||
|
|
@ -347,14 +410,26 @@ export const TableList: React.FC<any> = ({
|
|||
(e: any) => e?.name === name
|
||||
);
|
||||
const isSort =
|
||||
typeof col?.sortable === "boolean"
|
||||
name === "select"
|
||||
? false
|
||||
: typeof col?.sortable === "boolean"
|
||||
? col.sortable
|
||||
: true;
|
||||
const resize =
|
||||
name === "select"
|
||||
? false
|
||||
: typeof col?.resize === "boolean"
|
||||
? col.resize
|
||||
: true;
|
||||
return (
|
||||
<th
|
||||
{...{
|
||||
style: {
|
||||
width: col?.width
|
||||
width: !resize
|
||||
? `${col.width}px`
|
||||
: name === "select"
|
||||
? `${5}px`
|
||||
: col?.width
|
||||
? header.getSize() < col?.width
|
||||
? `${col.width}px`
|
||||
: header.getSize()
|
||||
|
|
@ -363,7 +438,13 @@ export const TableList: React.FC<any> = ({
|
|||
}}
|
||||
key={header.id}
|
||||
colSpan={header.colSpan}
|
||||
className="relative px-2 py-2 text-sm py-1 "
|
||||
className={cx(
|
||||
"relative px-2 py-2 text-sm py-1 uppercase",
|
||||
name === "select" &&
|
||||
css`
|
||||
max-width: 5px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div
|
||||
key={`${header.id}-label`}
|
||||
|
|
@ -398,7 +479,12 @@ export const TableList: React.FC<any> = ({
|
|||
isSort ? " cursor-pointer" : ""
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center flex-grow text-sm">
|
||||
<div
|
||||
className={cx(
|
||||
"flex flex-row items-center flex-grow text-sm capitalize",
|
||||
name === "select" ? "justify-center" : ""
|
||||
)}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
|
|
@ -438,13 +524,19 @@ export const TableList: React.FC<any> = ({
|
|||
header.column.resetSize(),
|
||||
onMouseDown: header.getResizeHandler(),
|
||||
onTouchStart: header.getResizeHandler(),
|
||||
className: `resizer w-0.5 bg-gray-300 ${
|
||||
className: cx(
|
||||
`resizer bg-[#b3c9fe] cursor-e-resize ${
|
||||
table.options.columnResizeDirection
|
||||
} ${
|
||||
header.column.getIsResizing()
|
||||
? "isResizing"
|
||||
: ""
|
||||
}`,
|
||||
css`
|
||||
width: 1px;
|
||||
cursor: e-resize !important;
|
||||
`
|
||||
),
|
||||
style: {
|
||||
transform:
|
||||
columnResizeMode === "onEnd" &&
|
||||
|
|
@ -474,15 +566,16 @@ export const TableList: React.FC<any> = ({
|
|||
<></>
|
||||
)}
|
||||
|
||||
<Table.Body className="divide-y divide-gray-200 bg-white">
|
||||
<Table.Body className="divide-y border-none bg-white">
|
||||
{table.getRowModel().rows.map((row, idx) => (
|
||||
<Table.Row
|
||||
key={row.id}
|
||||
className={cx(
|
||||
disabledHoverRow ? "" : "hover:bg-[#DBDBE7]",
|
||||
disabledHoverRow ? "" : "hover:bg-gray-100",
|
||||
css`
|
||||
height: 44px;
|
||||
`
|
||||
`,
|
||||
"border-none"
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
|
|
@ -542,11 +635,19 @@ export const TableList: React.FC<any> = ({
|
|||
</div>
|
||||
</div>
|
||||
<Pagination
|
||||
list={local}
|
||||
count={local.count}
|
||||
onNextPage={() => table.nextPage()}
|
||||
onPrevPage={() => table.previousPage()}
|
||||
disabledNextPage={!table.getCanNextPage()}
|
||||
disabledPrevPage={!table.getCanPreviousPage()}
|
||||
page={table.getState().pagination.pageIndex + 1}
|
||||
setPage={(page: any) => {
|
||||
setPagination({
|
||||
pageIndex: page,
|
||||
pageSize: 20,
|
||||
});
|
||||
}}
|
||||
countPage={table.getPageCount()}
|
||||
countData={local.data.length}
|
||||
take={take}
|
||||
|
|
@ -565,36 +666,83 @@ export const Pagination: React.FC<any> = ({
|
|||
disabledNextPage,
|
||||
disabledPrevPage,
|
||||
page,
|
||||
countPage,
|
||||
countData,
|
||||
take,
|
||||
count,
|
||||
list,
|
||||
setPage,
|
||||
onChangePage,
|
||||
}) => {
|
||||
const local = useLocal({
|
||||
page: 1 as any,
|
||||
pagination: [] as any,
|
||||
});
|
||||
useEffect(() => {
|
||||
local.page = page;
|
||||
local.pagination = getPagination(page, Math.ceil(count / 20));
|
||||
local.render();
|
||||
}, [page]);
|
||||
}, [page, count]);
|
||||
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 className=" border-t border-gray-300 tbl-pagination sticky text-sm bottom-0 right-0 w-full grid grid-cols-3 gap-4 justify-end text-sm bg-white pt-2">
|
||||
<div className="flex flex-row items-center text-gray-600">
|
||||
Showing {local.page * 20 - 19} to{" "}
|
||||
{list.data?.length >= 20
|
||||
? local.page * 20
|
||||
: local.page === 1 && Math.ceil(count / 20) === 1
|
||||
? list.data?.length
|
||||
: local.page * 20 - 19 + list.data?.length}{" "}
|
||||
of {formatMoney(getNumber(count))} results
|
||||
</div>
|
||||
<div className="flex flex-row justify-center">
|
||||
<div>
|
||||
<nav
|
||||
className="isolate inline-flex -space-x-px flex flex-row items-center gap-x-2"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
{local.pagination.map((e: any, idx: number) => {
|
||||
return (
|
||||
<div
|
||||
key={"page_" + idx}
|
||||
onClick={() => {
|
||||
if (e?.label !== "...") {
|
||||
local.page = getNumber(e?.label);
|
||||
local.render();
|
||||
onChangePage(local.page - 1);
|
||||
setPage(local.page - 1);
|
||||
list.reload();
|
||||
}
|
||||
}}
|
||||
className={cx(
|
||||
"text-sm px-2 py-1",
|
||||
e.active
|
||||
? "relative z-10 inline-flex items-center bg-primary font-semibold text-white rounded-md"
|
||||
: e?.label === "..."
|
||||
? "relative z-10 inline-flex items-center font-semibold text-gray-800 rounded-md"
|
||||
: "cursor-pointer relative z-10 inline-flex items-center hover:bg-gray-100 font-semibold text-gray-800 rounded-md"
|
||||
)}
|
||||
>
|
||||
{e?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-end">
|
||||
<div className="flex items-center flex-row gap-x-2 sm:mb-0 text-sm">
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!disabledPrevPage) {
|
||||
onPrevPage();
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
"inline-flex justify-center rounded p-1 ",
|
||||
className={cx(
|
||||
"flex flex-row items-center gap-x-2 justify-center rounded p-1 ",
|
||||
disabledPrevPage
|
||||
? "text-gray-200"
|
||||
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900"
|
||||
? "text-gray-200 border-gray-200 border px-2"
|
||||
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Previous page</span>
|
||||
<HiChevronLeft className="text-2xl" />
|
||||
<HiChevronLeft className="text-sm" />
|
||||
<span>Previous</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
|
|
@ -602,101 +750,55 @@ export const Pagination: React.FC<any> = ({
|
|||
onNextPage();
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
"inline-flex justify-center rounded p-1 ",
|
||||
className={cx(
|
||||
"flex flex-row items-center gap-x-2 justify-center rounded p-1 ",
|
||||
disabledNextPage
|
||||
? "text-gray-200"
|
||||
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900"
|
||||
? "text-gray-200 border-gray-200 border px-2"
|
||||
: "cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-900 border-gray-500 border px-2"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Next page</span>
|
||||
<HiChevronRight className="text-2xl" />
|
||||
<span>Next</span>
|
||||
<HiChevronRight className="text-sm" />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
const getPagination = (currentPage: number, totalPages: number) => {
|
||||
const pagination: { label: string; active: boolean }[] = [];
|
||||
const maxVisible = 5; // Jumlah maksimal elemen yang ditampilkan
|
||||
const halfRange = Math.floor((maxVisible - 3) / 2);
|
||||
|
||||
if (totalPages <= maxVisible) {
|
||||
// Jika total halaman lebih kecil dari batas, tampilkan semua halaman
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pagination.push({ label: i.toString(), active: i === currentPage });
|
||||
}
|
||||
} else {
|
||||
pagination.push({ label: "1", active: currentPage === 1 }); // Halaman pertama selalu ada
|
||||
|
||||
if (currentPage > halfRange + 2) {
|
||||
pagination.push({ label: "...", active: false }); // Awal titik-titik
|
||||
}
|
||||
|
||||
const startPage = Math.max(2, currentPage - halfRange);
|
||||
const endPage = Math.min(totalPages - 1, currentPage + halfRange);
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pagination.push({ label: i.toString(), active: i === currentPage });
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - halfRange - 1) {
|
||||
pagination.push({ label: "...", active: false }); // Akhir titik-titik
|
||||
}
|
||||
|
||||
pagination.push({
|
||||
label: totalPages.toString(),
|
||||
active: currentPage === totalPages,
|
||||
}); // Halaman terakhir selalu ada
|
||||
}
|
||||
|
||||
return pagination;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,349 @@
|
|||
import dayjs from "dayjs";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
COLORS,
|
||||
DATE_FORMAT,
|
||||
DEFAULT_COLOR,
|
||||
LANGUAGE,
|
||||
} from "./Datepicker/constants";
|
||||
import { ColorKeys, DatepickerType, Period } from "./Datepicker/types";
|
||||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { formatDate, nextMonth, previousMonth } from "./Datepicker/helpers";
|
||||
import useOnClickOutside from "./Datepicker/hooks";
|
||||
import DatepickerContext from "./Datepicker/contexts/DatepickerContext";
|
||||
import Calendar from "./Datepicker/components/Calendar";
|
||||
const CalenderFull: 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",
|
||||
onMark,
|
||||
onLoad,
|
||||
style,
|
||||
}) => {
|
||||
const local = useLocal({ open: false });
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
useEffect(() => {}, []);
|
||||
// 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(() => {
|
||||
firstGotoDate(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}>
|
||||
<Calendar
|
||||
date={firstDate}
|
||||
onClickPrevious={previousMonthFirst}
|
||||
onClickNext={nextMonthFirst}
|
||||
changeMonth={changeFirstMonth}
|
||||
changeYear={changeFirstYear}
|
||||
mode={mode}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
onMark={onMark}
|
||||
style={style}
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
</DatepickerContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalenderFull;
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
import dayjs from "dayjs";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import React, { useCallback, useContext } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { BG_COLOR, TEXT_COLOR } from "../../constants";
|
||||
import DatepickerContext from "../../contexts/DatepickerContext";
|
||||
|
|
@ -11,6 +17,8 @@ import {
|
|||
classNames as cn,
|
||||
} from "../../helpers";
|
||||
import { Period } from "../../types";
|
||||
import get from "lodash.get";
|
||||
import { getNumber } from "@/lib/utils/getNumber";
|
||||
|
||||
dayjs.extend(isBetween);
|
||||
|
||||
|
|
@ -26,16 +34,24 @@ interface Props {
|
|||
onClickPreviousDays: (day: number) => void;
|
||||
onClickDay: (day: number) => void;
|
||||
onClickNextDays: (day: number) => void;
|
||||
onIcon?: (day: number, date: Date) => any;
|
||||
onIcon?: (day: number, date: Date, data?: any) => any;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
const Days: React.FC<Props> = ({
|
||||
calendarData,
|
||||
onClickPreviousDays,
|
||||
onClickDay,
|
||||
onClickNextDays,
|
||||
onIcon,
|
||||
style,
|
||||
}) => {
|
||||
// Ref
|
||||
const calendarRef = useRef(null);
|
||||
const markRef = useRef(null);
|
||||
const [height, setHeight] = useState(0);
|
||||
const [heightItem, setHeightItem] = useState(0);
|
||||
const [maxItem, setMaxItem] = useState(0);
|
||||
const [width, setWidth] = useState(0);
|
||||
// Contexts
|
||||
const {
|
||||
primaryColor,
|
||||
|
|
@ -76,17 +92,13 @@ const Days: React.FC<Props> = ({
|
|||
) {
|
||||
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 ${
|
||||
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 ${
|
||||
className = ` ${BG_COLOR["500"][primaryColor]} text-white font-medium ${
|
||||
dayjs(fullDay).isSame(dayHover) && !period.start
|
||||
? "rounded-full"
|
||||
: "rounded-r-full"
|
||||
|
|
@ -241,13 +253,16 @@ const Days: React.FC<Props> = ({
|
|||
|
||||
const buttonClass = useCallback(
|
||||
(day: number, type: "current" | "next" | "previous") => {
|
||||
const baseClass =
|
||||
"flex items-center justify-center w-10 h-10 relative";
|
||||
let baseClass = `calender-day flex items-center justify-center ${
|
||||
style === "custom" ? " w-6 h-6 m-1" : "w-12 h-12 lg:w-10 lg:h-10"
|
||||
} relative`;
|
||||
if (type === "current") {
|
||||
return cn(
|
||||
baseClass,
|
||||
!activeDateData(day).active
|
||||
? hoverClassByDay(day)
|
||||
: style === "custom"
|
||||
? ""
|
||||
: activeDateData(day).className,
|
||||
isDateDisabled(day, type) && "text-gray-400 cursor-not-allowed"
|
||||
);
|
||||
|
|
@ -400,11 +415,90 @@ const Days: React.FC<Props> = ({
|
|||
}`;
|
||||
}
|
||||
const res = new Date(fullDay);
|
||||
return typeof onIcon === "function" ? onIcon(day, res) : null;
|
||||
return typeof onIcon === "function"
|
||||
? onIcon(day, res, {
|
||||
ref: calendarRef,
|
||||
height,
|
||||
maxItem,
|
||||
width,
|
||||
heightItem,
|
||||
})
|
||||
: null;
|
||||
};
|
||||
useEffect(() => {
|
||||
if (calendarRef?.current && markRef?.current) {
|
||||
const card = getNumber(get(calendarRef, "current.clientWidth"));
|
||||
const cardHeight = getNumber(get(markRef, "current.clientHeight"));
|
||||
const heightItem = 20; // perkiraan
|
||||
setWidth(card - 2);
|
||||
setHeight(cardHeight);
|
||||
setMaxItem(Math.floor(cardHeight / heightItem));
|
||||
setHeightItem(20);
|
||||
// setMaxItem
|
||||
const day = 3;
|
||||
const fullwidth = card * 7;
|
||||
const percent = (7 / 3) * 100;
|
||||
}
|
||||
}, [calendarRef.current, markRef.current]);
|
||||
return (
|
||||
<div className="grid grid-cols-7 gap-y-0.5 my-1">
|
||||
<div
|
||||
className={cx(
|
||||
"calender-days grid grid-cols-7 ",
|
||||
style === "custom" ? "" : " my-1 gap-y-0.5",
|
||||
css`
|
||||
z-index: 0;
|
||||
.calender-grid {
|
||||
// aspect-ratio: 1 / 1;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
{calendarData.days.previous.map((item, index) => (
|
||||
<div
|
||||
key={"prev_" + index}
|
||||
className={cx(
|
||||
"calender-grid flex flex-row",
|
||||
style === "custom"
|
||||
? "border-gray-200 hover:bg-gray-100 cursor-pointer"
|
||||
: ""
|
||||
)}
|
||||
onClick={() => {
|
||||
if (style === "custom") handleClickDay(item, "previous");
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-grow calender-day-wrap">
|
||||
{style === "custom" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
disabled={isDateDisabled(item, "previous")}
|
||||
className={`${buttonClass(item, "previous")}`}
|
||||
onMouseOver={() => {
|
||||
hoverDay(item, "previous");
|
||||
}}
|
||||
>
|
||||
<span className="relative">{item}</span>
|
||||
</button>
|
||||
<div className="flex flex-grow relative">
|
||||
{load_marker(item, "previous")}
|
||||
{/*
|
||||
{index === 1 && (
|
||||
<div
|
||||
className={cx(
|
||||
"hover:bg-gray-200 font-bold text-sm text-black px-2 absolute top-[27px] left-0 w-[196px] rounded-md",
|
||||
css`
|
||||
z-index: 1;
|
||||
`
|
||||
)}
|
||||
>
|
||||
1 more
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
|
|
@ -420,17 +514,57 @@ const Days: React.FC<Props> = ({
|
|||
{load_marker(item, "previous")}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{calendarData.days.current.map((item, index) => (
|
||||
<div
|
||||
key={"current_" + index}
|
||||
ref={index === 0 ? calendarRef : null}
|
||||
className={cx(
|
||||
"calender-grid flex flex-row",
|
||||
style === "custom"
|
||||
? activeDateData(item).active
|
||||
? "bg-blue-200/75 ring-1 cursor-pointer border-gray-200"
|
||||
: "hover:bg-gray-50 cursor-pointer border-gray-200 bg-white"
|
||||
: ""
|
||||
)}
|
||||
onClick={() => {
|
||||
if (style === "custom") handleClickDay(item, "current");
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-grow calender-day-wrap">
|
||||
{style === "custom" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
disabled={isDateDisabled(item, "current")}
|
||||
className={cx(
|
||||
`${buttonClass(item, "current")}`,
|
||||
item === 1 && "highlight"
|
||||
)}
|
||||
className={`${buttonClass(item, "current")}`}
|
||||
// onClick={() => handleClickDay(item, "current")}
|
||||
onMouseOver={() => {
|
||||
hoverDay(item, "current");
|
||||
}}
|
||||
>
|
||||
<span className="relative">{item}</span>
|
||||
</button>
|
||||
<div
|
||||
className="flex flex-grow relative "
|
||||
ref={index === 0 ? markRef : null}
|
||||
>
|
||||
{load_marker(item, "current")}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
disabled={isDateDisabled(item, "current")}
|
||||
className={`${buttonClass(item, "current")}`}
|
||||
onClick={() => handleClickDay(item, "current")}
|
||||
onMouseOver={() => {
|
||||
hoverDay(item, "current");
|
||||
|
|
@ -441,9 +575,43 @@ const Days: React.FC<Props> = ({
|
|||
{load_marker(item, "current")}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{calendarData.days.next.map((item, index) => (
|
||||
<div
|
||||
key={"next_" + index}
|
||||
className={cx(
|
||||
"calender-grid flex flex-row ",
|
||||
style === "custom"
|
||||
? "hover:bg-gray-100 cursor-pointer border-gray-200"
|
||||
: ""
|
||||
)}
|
||||
onClick={() => {
|
||||
if (style === "custom") handleClickDay(item, "next");
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col flex-grow calender-day-wrap">
|
||||
{style === "custom" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
disabled={isDateDisabled(item, "next")}
|
||||
className={`${buttonClass(item, "next")}`}
|
||||
onMouseOver={() => {
|
||||
hoverDay(item, "next");
|
||||
}}
|
||||
>
|
||||
<span className="relative">{item}</span>
|
||||
</button>
|
||||
<div>{load_marker(item, "next")}</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
key={index}
|
||||
|
|
@ -459,6 +627,10 @@ const Days: React.FC<Props> = ({
|
|||
{load_marker(item, "next")}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,13 +9,19 @@ import { RoundedButton } from "../utils";
|
|||
interface Props {
|
||||
currentMonth: number;
|
||||
clickMonth: (month: number) => void;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
const Months: React.FC<Props> = ({ currentMonth, clickMonth }) => {
|
||||
const Months: React.FC<Props> = ({ currentMonth, clickMonth, style }) => {
|
||||
const { i18n } = useContext(DatepickerContext);
|
||||
loadLanguageModule(i18n);
|
||||
return (
|
||||
<div className={"w-full grid grid-cols-2 gap-2 mt-2"}>
|
||||
<div
|
||||
className={cx(
|
||||
"w-full grid gap-2 mt-2",
|
||||
style === "custom" ? "uppercase grid-cols-2 p-4" : "grid-cols-2"
|
||||
)}
|
||||
>
|
||||
{MONTHS.map((item) => (
|
||||
<RoundedButton
|
||||
key={item}
|
||||
|
|
@ -24,8 +30,15 @@ const Months: React.FC<Props> = ({ currentMonth, clickMonth }) => {
|
|||
clickMonth(item);
|
||||
}}
|
||||
active={currentMonth === item}
|
||||
style={style}
|
||||
>
|
||||
{style === "custom" ? (
|
||||
<div className="px-2 py-1">
|
||||
{dayjs(`2022-${item}-01`).locale(i18n).format("MMMM")}
|
||||
</div>
|
||||
) : (
|
||||
<>{dayjs(`2022-${item}-01`).locale(i18n).format("MMM")}</>
|
||||
)}
|
||||
</RoundedButton>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@ 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 = () => {
|
||||
interface Props {
|
||||
style?: string;
|
||||
}
|
||||
const Week: React.FC<Props> = ({ style }) => {
|
||||
const { i18n, startWeekOn } = useContext(DatepickerContext);
|
||||
loadLanguageModule(i18n);
|
||||
const startDateModifier = useMemo(() => {
|
||||
|
|
@ -33,11 +35,25 @@ const Week: React.FC = () => {
|
|||
}, [startWeekOn]);
|
||||
|
||||
return (
|
||||
<div className=" grid grid-cols-7 border-b border-gray-300 dark:border-gray-700 py-2">
|
||||
<div
|
||||
className={cx(
|
||||
" grid grid-cols-7 border-b dark:border-gray-700",
|
||||
style === "custom"
|
||||
? "sticky top-0 bg-white z-99 border-gray-200"
|
||||
: "border-gray-300 py-2",
|
||||
style === "custom" &&
|
||||
css`
|
||||
z-index: 99;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{DAYS.map((item) => (
|
||||
<div
|
||||
key={item}
|
||||
className="tracking-wide text-gray-500 text-center"
|
||||
className={cx(
|
||||
"tracking-wide text-gray-500 text-center",
|
||||
style === "custom" && " border-r border-gray-200 py-2"
|
||||
)}
|
||||
>
|
||||
{ucFirst(
|
||||
shortString(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface Props {
|
|||
minYear: number | null;
|
||||
maxYear: number | null;
|
||||
clickYear: (data: number) => void;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
const Years: React.FC<Props> = ({
|
||||
|
|
@ -19,6 +20,7 @@ const Years: React.FC<Props> = ({
|
|||
minYear,
|
||||
maxYear,
|
||||
clickYear,
|
||||
style,
|
||||
}) => {
|
||||
const { dateLooking } = useContext(DatepickerContext);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import Week from "./Week";
|
|||
import Years from "./Years";
|
||||
|
||||
import { DateType } from "../../types";
|
||||
import { Popover } from "@/lib/components/Popover/Popover";
|
||||
|
||||
interface Props {
|
||||
date: dayjs.Dayjs;
|
||||
|
|
@ -44,7 +45,9 @@ interface Props {
|
|||
changeMonth: (month: number) => void;
|
||||
changeYear: (year: number) => void;
|
||||
mode?: "monthly" | "daily";
|
||||
onMark?: (day: number, date: Date) => any;
|
||||
onMark?: (day: number, date: Date, data?: any) => any;
|
||||
style?: "custom" | "prasi";
|
||||
onLoad?: (e?: any) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const Calendar: React.FC<Props> = ({
|
||||
|
|
@ -57,6 +60,8 @@ const Calendar: React.FC<Props> = ({
|
|||
changeYear,
|
||||
onMark,
|
||||
mode = "daily",
|
||||
style = "prasi",
|
||||
onLoad,
|
||||
}) => {
|
||||
// Contexts
|
||||
const {
|
||||
|
|
@ -77,6 +82,8 @@ const Calendar: React.FC<Props> = ({
|
|||
const [showMonths, setShowMonths] = useState(false);
|
||||
const [showYears, setShowYears] = useState(false);
|
||||
const [year, setYear] = useState(date.year());
|
||||
const [openPopover, setOpenPopover] = useState(false);
|
||||
const [openPopoverYear, setOpenPopoverYear] = useState(false);
|
||||
useEffect(() => {
|
||||
if (mode === "monthly") {
|
||||
setShowMonths(true);
|
||||
|
|
@ -90,7 +97,12 @@ const Calendar: React.FC<Props> = ({
|
|||
getNumberOfDay(getFirstDayInMonth(date).ddd, startWeekOn)
|
||||
);
|
||||
}, [date, startWeekOn]);
|
||||
|
||||
const previousDate = useCallback(() => {
|
||||
const day = getLastDaysInMonth(
|
||||
previousMonth(date),
|
||||
getNumberOfDay(getFirstDayInMonth(date).ddd, startWeekOn)
|
||||
);
|
||||
}, [date, startWeekOn]);
|
||||
const current = useCallback(() => {
|
||||
return getDaysInMonth(formatDate(date));
|
||||
}, [date]);
|
||||
|
|
@ -245,17 +257,79 @@ const Calendar: React.FC<Props> = ({
|
|||
setYear(date.year());
|
||||
}, [date]);
|
||||
|
||||
const getMonth = (month?: string) => {
|
||||
const value: any = date;
|
||||
const currentDate: any = new Date(value);
|
||||
const previousMonthDate = new Date(currentDate);
|
||||
previousMonthDate.setDate(1);
|
||||
switch (month) {
|
||||
case "before":
|
||||
previousMonthDate.setMonth(currentDate.getMonth() - 1);
|
||||
break;
|
||||
case "after":
|
||||
previousMonthDate.setMonth(currentDate.getMonth() + 1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return previousMonthDate;
|
||||
};
|
||||
// Variables
|
||||
const calendarData = useMemo(() => {
|
||||
return {
|
||||
date: date,
|
||||
days: {
|
||||
const data = {
|
||||
previous: previous(),
|
||||
current: current(),
|
||||
next: next(),
|
||||
};
|
||||
const result = {
|
||||
date: date,
|
||||
days: data,
|
||||
time: {
|
||||
previous: data?.previous?.length
|
||||
? data.previous.map((e) => {
|
||||
return new Date(
|
||||
getMonth("before").getFullYear(),
|
||||
getMonth("before").getMonth(),
|
||||
e
|
||||
);
|
||||
})
|
||||
: [],
|
||||
current: data?.current?.length
|
||||
? data.current.map((e) => {
|
||||
return new Date(
|
||||
getMonth().getFullYear(),
|
||||
getMonth().getMonth(),
|
||||
e
|
||||
);
|
||||
})
|
||||
: [],
|
||||
next: data?.next?.length
|
||||
? data.next.map((e) => {
|
||||
return new Date(
|
||||
getMonth("after").getFullYear(),
|
||||
getMonth("after").getMonth(),
|
||||
e
|
||||
);
|
||||
})
|
||||
: [],
|
||||
},
|
||||
};
|
||||
return result;
|
||||
}, [current, date, next, previous]);
|
||||
useEffect(() => {
|
||||
if (typeof onLoad === "function") {
|
||||
const run = async () => {
|
||||
if (typeof onLoad === "function") {
|
||||
const param = dayjs(formatDate(date)).toDate();
|
||||
await onLoad({
|
||||
date: param,
|
||||
calender: calendarData,
|
||||
});
|
||||
}
|
||||
};
|
||||
run();
|
||||
}
|
||||
}, [calendarData, date]);
|
||||
const minYear = React.useMemo(
|
||||
() => (minDate && dayjs(minDate).isValid() ? dayjs(minDate).year() : null),
|
||||
[minDate]
|
||||
|
|
@ -264,19 +338,54 @@ const Calendar: React.FC<Props> = ({
|
|||
() => (maxDate && dayjs(maxDate).isValid() ? dayjs(maxDate).year() : null),
|
||||
[maxDate]
|
||||
);
|
||||
const isCustom = style === "custom";
|
||||
|
||||
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",
|
||||
"w-full md:w-[296px] md:min-w-[296px] calender",
|
||||
isCustom && "flex-grow"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cx(
|
||||
"flex items-stretch ",
|
||||
isCustom ? "" : "space-x-1.5 px-2 py-1.5 flex-col",
|
||||
css`
|
||||
border-bottom: 1px solid #d1d5db;
|
||||
`
|
||||
)}
|
||||
>
|
||||
{style === "custom" ? (
|
||||
<div className="flex flex-row items-center px-2 py-2 justify-between w-full">
|
||||
<div className="flex flex-row gap-x-2 items-center">
|
||||
<div className="flex flex-row gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClickPrevious}
|
||||
className="flex items-center justify-center rounded-l-md py-2 pl-3 pr-4 text-gray-400 hover:text-gray-500 focus:relative md:w-9 md:px-2 md:hover:bg-gray-50"
|
||||
>
|
||||
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClickNext}
|
||||
className="flex items-center justify-center rounded-r-md py-2 pl-4 pr-3 text-gray-400 hover:text-gray-500 focus:relative md:w-9 md:px-2 md:hover:bg-gray-50"
|
||||
>
|
||||
<ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-base font-semibold text-gray-900 capitalize flex flex-row">
|
||||
{calendarData.date.locale(i18n).format("MMMM")}{" "}
|
||||
{calendarData.date.year()}
|
||||
</div>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{!showMonths && !showYears && (
|
||||
<div className="flex-none flex flex-row items-center">
|
||||
<div className="flex-none">
|
||||
<RoundedButton roundedFull={true} onClick={onClickPrevious}>
|
||||
<ChevronLeftIcon className="h-5 w-5" />
|
||||
</RoundedButton>
|
||||
|
|
@ -284,7 +393,7 @@ const Calendar: React.FC<Props> = ({
|
|||
)}
|
||||
|
||||
{showYears && (
|
||||
<div className="flex-none flex flex-row items-center">
|
||||
<div className="flex-none">
|
||||
<RoundedButton
|
||||
roundedFull={true}
|
||||
onClick={() => {
|
||||
|
|
@ -315,13 +424,13 @@ const Calendar: React.FC<Props> = ({
|
|||
hideMonths();
|
||||
}}
|
||||
>
|
||||
<div className="">{calendarData.date.year()}</div>
|
||||
<div className="py-2">{calendarData.date.year()}</div>
|
||||
</RoundedButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showYears && (
|
||||
<div className="flex-none flex flex-row items-center">
|
||||
<div className="flex-none">
|
||||
<RoundedButton
|
||||
roundedFull={true}
|
||||
onClick={() => {
|
||||
|
|
@ -334,18 +443,26 @@ const Calendar: React.FC<Props> = ({
|
|||
)}
|
||||
|
||||
{!showMonths && !showYears && (
|
||||
<div className="flex-none flex flex-row items-center" >
|
||||
<div className="flex-none">
|
||||
<RoundedButton roundedFull={true} onClick={onClickNext}>
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</RoundedButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cx("mt-0.5 min-h-[285px]")}>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
isCustom ? "flex-grow" : "min-h-[285px]",
|
||||
"mt-0.5 calender-body"
|
||||
)}
|
||||
>
|
||||
{showMonths && (
|
||||
<Months
|
||||
currentMonth={calendarData.date.month() + 1}
|
||||
clickMonth={clickMonth}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -361,27 +478,19 @@ const Calendar: React.FC<Props> = ({
|
|||
|
||||
{!showMonths && !showYears && (
|
||||
<>
|
||||
<Week />
|
||||
<Week style={style} />
|
||||
|
||||
<Days
|
||||
calendarData={calendarData}
|
||||
onClickPreviousDays={clickPreviousDays}
|
||||
onClickDay={clickDay}
|
||||
onClickNextDays={clickNextDays}
|
||||
onIcon={(day, date) => {
|
||||
if(typeof onMark === "function"){
|
||||
return onMark(day, date)
|
||||
style={style}
|
||||
onIcon={(day, date, data) => {
|
||||
if (typeof onMark === "function") {
|
||||
return onMark(day, date, data);
|
||||
}
|
||||
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 <></>
|
||||
return <></>;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ interface Button {
|
|||
roundedFull?: boolean;
|
||||
padding?: string;
|
||||
active?: boolean;
|
||||
style?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const DateIcon: React.FC<IconProps> = ({ className = "w-6 h-6" }) => {
|
||||
|
|
@ -205,27 +207,28 @@ export const RoundedButton: React.FC<Button> = ({
|
|||
onClick,
|
||||
disabled,
|
||||
roundedFull = false,
|
||||
padding = "py-[0.55rem]",
|
||||
padding = "",
|
||||
active = false,
|
||||
style,
|
||||
className = "rounded-full",
|
||||
}) => {
|
||||
// Contexts
|
||||
const { primaryColor } = useContext(DatepickerContext);
|
||||
|
||||
// Functions
|
||||
const getClassName = useCallback(() => {
|
||||
const darkClass =
|
||||
"";
|
||||
const activeClass = active
|
||||
? "font-semibold bg-gray-50 "
|
||||
: "";
|
||||
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`;
|
||||
? `w-full tracking-wide ${darkClass} ${activeClass} transition-all duration-300 ${
|
||||
style === "custom" ? "px-2" : "uppercase p-[0.45rem] py-[0.55rem]"
|
||||
} hover:bg-gray-100 focus:ring-1`
|
||||
: `${darkClass} ${activeClass} transition-all duration-300 hover:bg-gray-100 focus:ring-1`;
|
||||
const buttonFocusColor =
|
||||
BUTTON_COLOR.focus[primaryColor as keyof typeof BUTTON_COLOR.focus];
|
||||
const disabledClass = disabled ? "line-through" : "";
|
||||
|
||||
return `${defaultClass} ${buttonFocusColor} ${disabledClass}`;
|
||||
return `${defaultClass} ${buttonFocusColor} ${disabledClass} ${className}`;
|
||||
}, [disabled, padding, primaryColor, roundedFull, active]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -122,6 +122,21 @@ export function getFirstElementsInArray(array: number[] = [], size = 0) {
|
|||
return array.slice(0, size);
|
||||
}
|
||||
|
||||
export function getLastElementsDateInArray(array: any[] = [], size = 0) {
|
||||
const result: any[] = [];
|
||||
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 getLastElementsInArray(array: number[] = [], size = 0) {
|
||||
const result: number[] = [];
|
||||
if (Array.isArray(array) && size > 0) {
|
||||
|
|
@ -194,7 +209,9 @@ export function getNumberOfDay(
|
|||
export function getLastDaysInMonth(date: dayjs.Dayjs | string, size = 0) {
|
||||
return getLastElementsInArray(getDaysInMonth(date), size);
|
||||
}
|
||||
|
||||
export function getLastDateInMonth(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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,10 @@ export type PopoverDirectionType = "up" | "down";
|
|||
export interface DatepickerType {
|
||||
primaryColor?: ColorKeys;
|
||||
value: DateValueType;
|
||||
onChange: (value: DateValueType, e?: HTMLInputElement | null | undefined) => void;
|
||||
onChange: (
|
||||
value: DateValueType,
|
||||
e?: HTMLInputElement | null | undefined
|
||||
) => void;
|
||||
useRange?: boolean;
|
||||
showFooter?: boolean;
|
||||
showShortcuts?: boolean;
|
||||
|
|
@ -83,9 +86,9 @@ export interface DatepickerType {
|
|||
startWeekOn?: string | null;
|
||||
popoverDirection?: PopoverDirectionType;
|
||||
mode?: "daily" | "monthly";
|
||||
onMark?: (day: number, date: Date) => any;
|
||||
onLoad?: () => Promise<void>
|
||||
|
||||
onMark?: (day: number, date: Date, data?: any) => any;
|
||||
onLoad?: (day: any) => Promise<void>;
|
||||
style?: "custom" | "prasi";
|
||||
}
|
||||
|
||||
export type ColorKeys = (typeof COLORS)[number]; // "blue" | "orange"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { useLocal } from "@/lib/utils/use-local";
|
||||
import { FC, ReactElement, useEffect, useTransition } from "react";
|
||||
import { Popover } from "../Popover/Popover";
|
||||
import { TypeColor } from "../form/field/TypeColor";
|
||||
|
||||
export const FieldColorPicker: FC<{
|
||||
children: ReactElement;
|
||||
value?: string;
|
||||
update: (value: string) => void;
|
||||
open: boolean;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
showHistory?: boolean;
|
||||
}> = ({ children, value, update, open, onClose, onOpen, showHistory }) => {
|
||||
const local = useLocal({ show: open || false });
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
local.show = open || false;
|
||||
local.render();
|
||||
}
|
||||
}, [value, open]);
|
||||
const [_, tx] = useTransition();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={local.show}
|
||||
onOpenChange={(open: any) => {
|
||||
local.show = open;
|
||||
if (open && onOpen) {
|
||||
onOpen();
|
||||
} else if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
local.render();
|
||||
}}
|
||||
backdrop={false}
|
||||
popoverClassName="rounded-md p-2 text-sm bg-white shadow-2xl border border-slate-300"
|
||||
content={
|
||||
<TypeColor
|
||||
value={value}
|
||||
showHistory={showHistory}
|
||||
onClose={() => {
|
||||
local.show = false;
|
||||
local.render();
|
||||
if (onClose) onClose();
|
||||
}}
|
||||
onChangePicker={(color: any) => {
|
||||
tx(() => {
|
||||
if (color.indexOf("NaN") < 0) {
|
||||
update(color);
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
local.show = true;
|
||||
local.render();
|
||||
if (onOpen) onOpen();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
@ -17,8 +17,6 @@ export const PinterestLayout: React.FC<{
|
|||
});
|
||||
return columns;
|
||||
};
|
||||
const layout_grid = new Array(col);
|
||||
const columns = createColumns(data, col); // Membagi data ke dalam kolom
|
||||
const local = useLocal({
|
||||
data: [] as any[],
|
||||
ids: {
|
||||
|
|
@ -33,8 +31,6 @@ export const PinterestLayout: React.FC<{
|
|||
const targetColumn = index % col; // Menentukan kolom target berdasarkan indeks
|
||||
columns[targetColumn].push(item); // Memasukkan elemen ke kolom yang sesuai
|
||||
});
|
||||
|
||||
console.log("Columns:", columns); // Debugging
|
||||
local.data = columns;
|
||||
local.render();
|
||||
}, [data, col]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
const AccordionTriggerCustom: React.FC<any> = (
|
||||
{ className, onRightLabel, children, ...props },
|
||||
ref
|
||||
) => (
|
||||
<AccordionPrimitive.Header className="flex flex-row items-center">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</AccordionPrimitive.Trigger>
|
||||
{typeof onRightLabel === "function" && onRightLabel()}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export {
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
AccordionContent,
|
||||
AccordionTriggerCustom,
|
||||
};
|
||||
|
|
@ -18,7 +18,7 @@ export const Alert: FC<any> = ({
|
|||
children,
|
||||
className,
|
||||
content,
|
||||
msg
|
||||
msg,
|
||||
}) => {
|
||||
const message: any = {
|
||||
save: "Your data will be saved securely. You can update it at any time if needed.",
|
||||
|
|
@ -35,10 +35,9 @@ export const Alert: FC<any> = ({
|
|||
) : (
|
||||
<>
|
||||
<AlertDialogHeader>
|
||||
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{message?.[type] || msg}
|
||||
{msg || message?.[type]}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { FC } from "react";
|
||||
|
||||
export const ButtonRichText: FC<any> = ({
|
||||
children,
|
||||
active,
|
||||
onClick,
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
className={cx(
|
||||
active ? "is-active bg-gray-200" : "",
|
||||
"text-black text-sm p-1 hover:bg-gray-200 rounded-md px-2 "
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
@ -9,16 +9,15 @@ 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",
|
||||
"cursor-pointer px-4 py-1.5 group relative flex items-stretch justify-center p-0.5 text-center 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",
|
||||
"border 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",
|
||||
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:
|
||||
|
|
@ -58,8 +57,12 @@ const ButtonBetter = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
);
|
||||
}
|
||||
);
|
||||
const ButtonContainer: FC<any> = ({ children, className, variant = "default" }) => {
|
||||
const vr = variant ? variant: "default"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import api from "../utils/axios";
|
||||
import { userRoleMe } from "../utils/getAccess";
|
||||
|
||||
export const userToken = async () => {
|
||||
const user = localStorage.getItem("user");
|
||||
if (user) {
|
||||
const w = window as any;
|
||||
w.user = JSON.parse(user);
|
||||
}else{
|
||||
const res = await api.get(`${process.env.NEXT_PUBLIC_API_PORTAL}/api/check-jwt-token`);
|
||||
const jwt = res.data.data;
|
||||
console.log({jwt})
|
||||
if (!jwt) return ;
|
||||
try {
|
||||
await api.post(process.env.NEXT_PUBLIC_BASE_URL + "/api/cookies", {
|
||||
token: jwt,
|
||||
});
|
||||
const user = await api.get(
|
||||
`${process.env.NEXT_PUBLIC_API_PORTAL}/api/users/me`
|
||||
);
|
||||
const us = user.data.data;
|
||||
console.log({us})
|
||||
if (us) {
|
||||
localStorage.setItem("user", JSON.stringify(user.data.data));
|
||||
const roles = await userRoleMe();
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
console.log({res})
|
||||
}
|
||||
};
|
||||
16
package.json
16
package.json
|
|
@ -6,6 +6,7 @@
|
|||
"@emotion/css": "^11.13.5",
|
||||
"@faker-js/faker": "^9.2.0",
|
||||
"@floating-ui/react": "^0.26.28",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.3",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
|
|
@ -13,6 +14,18 @@
|
|||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@react-pdf/renderer": "^4.1.5",
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@tiptap/extension-color": "^2.11.2",
|
||||
"@tiptap/extension-link": "^2.11.2",
|
||||
"@tiptap/extension-list-item": "^2.11.2",
|
||||
"@tiptap/extension-table": "^2.11.2",
|
||||
"@tiptap/extension-table-cell": "^2.11.2",
|
||||
"@tiptap/extension-table-header": "^2.11.2",
|
||||
"@tiptap/extension-table-row": "^2.11.2",
|
||||
"@tiptap/extension-text-align": "^2.11.2",
|
||||
"@tiptap/extension-underline": "^2.11.2",
|
||||
"@tiptap/pm": "^2.11.2",
|
||||
"@tiptap/react": "^2.11.2",
|
||||
"@tiptap/starter-kit": "^2.11.2",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.uniqby": "^4.7.9",
|
||||
|
|
@ -35,10 +48,12 @@
|
|||
"lodash.uniqby": "^4.7.0",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next": "15.0.3",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-slick": "^0.30.2",
|
||||
"sonner": "^1.7.0",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"uuid": "^11.0.3",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
|
|
@ -47,6 +62,7 @@
|
|||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-slick": "^0.23.13",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"postcss": "^8",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import get from "lodash.get";
|
||||
import api from "./axios";
|
||||
|
||||
type apixType = {
|
||||
port: "portal" | "recruitment" | "mpp";
|
||||
path: string;
|
||||
method?: "get" | "delete" | "post" | "put";
|
||||
data?: any;
|
||||
value?: any;
|
||||
validate?: "object" | "array" | "dropdown";
|
||||
keys?: {
|
||||
value?: string;
|
||||
label: string | ((item: any) => string);
|
||||
};
|
||||
};
|
||||
export const apix = async ({
|
||||
port = "portal",
|
||||
method = "get",
|
||||
data,
|
||||
value,
|
||||
path,
|
||||
validate = "object",
|
||||
keys,
|
||||
}: apixType) => {
|
||||
const root_url = `${
|
||||
port === "portal"
|
||||
? process.env.NEXT_PUBLIC_API_PORTAL
|
||||
: port === "recruitment"
|
||||
? process.env.NEXT_PUBLIC_API_RECRUITMENT
|
||||
: port === "mpp"
|
||||
? process.env.NEXT_PUBLIC_API_MPP
|
||||
: ""
|
||||
}${path}`;
|
||||
let result = null as any;
|
||||
try {
|
||||
try {
|
||||
switch (method) {
|
||||
case "get":
|
||||
result = await api.get(root_url);
|
||||
break;
|
||||
|
||||
case "post":
|
||||
result = await api.post(root_url, data);
|
||||
break;
|
||||
|
||||
case "put":
|
||||
result = await api.put(root_url, data);
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
result = await api.delete(root_url, data);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported HTTP method: ${method}`);
|
||||
}
|
||||
} catch (ex: any) {
|
||||
console.error(
|
||||
"API Error:",
|
||||
get(ex, "response.data.meta.message") || ex.message
|
||||
);
|
||||
}
|
||||
const val = get(result, value);
|
||||
return validate === "object"
|
||||
? get(result, value)
|
||||
: validate === "dropdown" && Array.isArray(get(result, value))
|
||||
? val.map((e: any) => {
|
||||
return {
|
||||
value: keys?.value ? get(e, keys?.value) : get(e, "id"),
|
||||
label:
|
||||
typeof keys?.label === "function"
|
||||
? keys.label(e)
|
||||
: keys?.label
|
||||
? get(e, keys?.label)
|
||||
: null,
|
||||
};
|
||||
})
|
||||
: Array.isArray(get(result, value))
|
||||
? get(result, value)
|
||||
: [];
|
||||
} catch (error: any) {
|
||||
console.error("API Error:", error.response || error.message);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
export const cloneFM = (fm: any, row: any) => {
|
||||
// const result -
|
||||
return {
|
||||
...fm,
|
||||
data: row
|
||||
}
|
||||
}
|
||||
data: row,
|
||||
render: fm.render,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
export const labelDocumentType = (value?: string) => {
|
||||
switch (value) {
|
||||
case "ADMINISTRATIVE_SELECTION":
|
||||
return "Administrative";
|
||||
break;
|
||||
case "TEST":
|
||||
return "Test";
|
||||
break;
|
||||
case "INTERVIEW":
|
||||
return "Interview";
|
||||
break;
|
||||
case "FGD":
|
||||
return "FGD";
|
||||
break;
|
||||
case "SURAT_PENGANTAR_MASUK":
|
||||
return "Surat Pengantar Masuk";
|
||||
break;
|
||||
case "SURAT_IZIN_ORANG_TUA":
|
||||
return "Surat Izin Orang Tua";
|
||||
break;
|
||||
case "FINAL_INTERVIEW":
|
||||
return "Final Interview";
|
||||
break;
|
||||
|
||||
case "KARYAWAN_TETAP":
|
||||
return "Karyawan Tetap";
|
||||
case "OFFERING_LETTER":
|
||||
return "Offering Letter";
|
||||
break;
|
||||
|
||||
case "CONTRACT_DOCUMENT":
|
||||
return "Contract Document";
|
||||
break;
|
||||
|
||||
case "DOCUMENT_CHECKING":
|
||||
return "Document Checking";
|
||||
break;
|
||||
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
|
@ -5,21 +5,30 @@ type EventActions = "before-onload" | "onload-param" | string;
|
|||
export const events = async (action: EventActions, data: any) => {
|
||||
switch (action) {
|
||||
case "onload-param":
|
||||
|
||||
const params = {
|
||||
let params = {
|
||||
...data,
|
||||
page: get(data, "paging"),
|
||||
page_size: get(data, "take"),
|
||||
search: get(data, "search")
|
||||
search: get(data, "search"),
|
||||
};
|
||||
delete params["paging"]
|
||||
delete params["take"]
|
||||
return generateQueryString(params)
|
||||
return
|
||||
params = {
|
||||
...params,
|
||||
};
|
||||
if (params?.sort) {
|
||||
params = {
|
||||
...params,
|
||||
...params?.sort,
|
||||
};
|
||||
}
|
||||
delete params["sort"];
|
||||
delete params["paging"];
|
||||
delete params["take"];
|
||||
return generateQueryString(params);
|
||||
return;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue