banyak bos

This commit is contained in:
faisolavolut 2025-01-17 14:06:58 +07:00
parent bb0e6d38f1
commit 31680f29c7
32 changed files with 3008 additions and 586 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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>
);
};

View File

@ -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}

View File

@ -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>
);
};

View File

@ -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"
>
&times;
</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>
);
};

View File

@ -446,6 +446,10 @@ export const Typeahead: FC<{
}
local.open = open;
local.render();
if (!open) {
resetSearch();
}
}}
showEmpty={!allow_new}
className={popupClassName}

View File

@ -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`);
}}

View File

@ -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>
)}
@ -217,43 +312,24 @@ const SidebarTree: React.FC<TreeMenuProps> = ({ data, minimaze, mini }) => {
<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="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" : ""
)}
>

View File

@ -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&nbsp;
<span className="font-semibold text-gray-900">{page}</span>
&nbsp;of&nbsp;
<span className="font-semibold text-gray-900">{countPage}</span>
</span>
<span className="flex items-center pl-2 text-black gap-x-2">
| Go to page:
<form
onSubmit={(e) => {
e.preventDefault();
const page = Number(local.page);
if (!page) {
local.page = 0;
} else if (page > countPage) {
local.page = countPage;
}
local.render();
onChangePage(local.page - 1);
}}
>
<Input
type="number"
min="1"
max={countPage}
value={local.page}
onChange={(e) => {
local.page = e.target.value;
local.render();
debouncedHandler(() => {
const page = Number(local.page);
if (!page) {
local.page = 0;
} else if (page > countPage) {
local.page = countPage;
}
local.render();
onChangePage(local.page - 1);
}, 1500);
}}
/>
</form>
</span>
</div>
<div className="flex items-center space-x-3 hidden">
{!disabledPrevPage ? (
<>
<div
onClick={() => {
if (!disabledPrevPage) {
onPrevPage();
}
}}
className={classNames(
"cursor-pointer inline-flex flex-1 items-center justify-center rounded-lg bg-primary px-3 py-2 text-center text-md font-medium text-white hover:bg-primary focus:ring-4 focus:ring-primary-300"
)}
>
<HiChevronLeft className="mr-1 text-base" />
Previous
</div>
</>
) : (
<></>
)}
{!disabledNextPage ? (
<>
<div
onClick={() => {
if (!disabledNextPage) {
onNextPage();
}
}}
className={classNames(
"cursor-pointer inline-flex flex-1 items-center justify-center rounded-lg bg-primary px-3 py-2 text-center text-md font-medium text-white hover:bg-primary focus:ring-4 focus:ring-primary-300"
)}
>
Next
<HiChevronRight className="ml-1 text-base" />
</div>
</>
) : (
<></>
)}
</div>
</div>
);
};
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;
};

View File

@ -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;

View File

@ -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>
);

View File

@ -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>

View File

@ -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(

View File

@ -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);

View File

@ -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) => {
style={style}
onIcon={(day, date, data) => {
if (typeof onMark === "function") {
return onMark(day, date)
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 <></>;
}}
/>
</>

View File

@ -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 (

View File

@ -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);
}

View File

@ -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"

View File

@ -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>
);
};

View File

@ -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]);

View File

@ -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,
};

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>

31
helpers/user.ts Normal file
View File

@ -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})
}
};

View File

@ -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",

85
utils/apix.ts Normal file
View File

@ -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;
}
};

View File

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

42
utils/document_type.ts Normal file
View File

@ -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;
}
};

View File

@ -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;
};