From 643659a26c7e5571fa1c183ed939d8a43817925c Mon Sep 17 00:00:00 2001 From: faisolavolut Date: Thu, 20 Feb 2025 08:26:43 +0700 Subject: [PATCH] feat: enhance form handling and loading states across components --- components/Popover/Popover.tsx | 1 + components/form/Field.tsx | 11 +- components/form/field/FilePreview.tsx | 157 ++++ components/form/field/TypeUpload.tsx | 4 + components/form/field/TypeUploadMulti.tsx | 148 +++- components/form/field/TypeaheadBetter.tsx | 836 ++++++++++++++++++++++ components/tablelist/List.tsx | 59 +- components/ui/Progress.tsx | 11 +- utils/apix.ts | 22 +- utils/convetForm.ts | 14 + 10 files changed, 1197 insertions(+), 66 deletions(-) create mode 100644 components/form/field/TypeaheadBetter.tsx create mode 100644 utils/convetForm.ts diff --git a/components/Popover/Popover.tsx b/components/Popover/Popover.tsx index a7049aa..d74f6fc 100644 --- a/components/Popover/Popover.tsx +++ b/components/Popover/Popover.tsx @@ -219,6 +219,7 @@ export function Popover({ Promise | any; + onDelete?: (item: any) => Promise | any; type?: | "rating" | "color" @@ -73,6 +75,8 @@ export const Field: React.FC<{ allowNew, unique = true, tooltip, + valueKey, + onDelete, }) => { let result = null; const field = useLocal({ @@ -182,6 +186,7 @@ export const Field: React.FC<{ "single-checkbox", "radio", "checkbox", + "multi-upload", ].includes(type) && css` border: 0px !important; @@ -197,7 +202,7 @@ export const Field: React.FC<{
) : ["dropdown"].includes(type) ? ( @@ -354,7 +361,7 @@ export const Field: React.FC<{
); }; +export const FilePreviewBetter = ({ + url, + disabled, + filename, +}: { + url: any; + disabled?: boolean; + filename?: string; +}) => { + const file: any = extractFileInfo(filename || url); + const color = colorOfExtension(file.extension); + let content = ( +
+ {file.extension} +
+ ); + if ( + [".png", ".jpeg", ".jpg", ".webp"].find((e) => file?.fullname.endsWith(e)) + ) { + content = ( +
+ { + let _url = siteurl(url || ""); + window.open(_url, "_blank"); + }} + className={cx( + "rounded-md w-8 h-8 object-cover", + css` + &:hover { + outline: 2px solid #1c4ed8; + } + `, + css` + background-image: linear-gradient( + 45deg, + #ccc 25%, + transparent 25% + ), + linear-gradient(135deg, #ccc 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #ccc 75%), + linear-gradient(135deg, transparent 75%, #ccc 75%); + background-size: 25px 25px; /* Must be a square */ + background-position: 0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px; /* Must be half of one side of the square */ + ` + )} + src={url} + /> +
+ ); + } + + return ( + <> + {file.extension && ( +
{ + window.open(url, "_blank"); + }} + > +
+
{content}
+
+ {file?.name} +
+
+ +
+ +
+
+ )} + + ); +}; function darkenColor(color: string, factor: number = 0.5): string { const rgb = hexToRgb(color); const r = Math.floor(rgb.r * factor); @@ -314,6 +419,26 @@ const getFileName = (url: string) => { return { name, extension, fullname }; }; +const extractFileInfo = (url: string) => { + let fileName = url.split("/").pop(); + if (fileName) { + let parts = fileName.split("."); + let extension = parts.length > 1 ? parts.pop() : ""; + let name = parts.join("."); + + return { + name: name, + fullname: fileName, + extension: extension, + }; + } else { + return { + name: null, + fullname: null, + extension: null, + }; + } +}; export const ImgThumb = ({ className, url, @@ -361,3 +486,35 @@ export const ImgThumb = ({
); }; + +const getRandomColorPair = () => { + const colors = [ + { color: "#dc2626", background: "#fbd5d5" }, + { color: "#2563eb", background: "#dbeafe" }, + { color: "#16a34a", background: "#dcfce7" }, + { color: "#6b7280", background: "#f3f4f6" }, + { color: "#7c3aed", background: "#ede9fe" }, + { color: "#f97316", background: "#ffedd5" }, + { color: "#0d9488", background: "#ccfbf1" }, + { color: "#9333ea", background: "#e9d5ff" }, + { color: "#eab308", background: "#fef9c3" }, + ]; + return colors[Math.floor(Math.random() * colors.length)]; +}; + +const colorOfExtension = (extension: string) => { + const colorMap: any = { + pdf: { color: "#dc2626", background: "#fbd5d5" }, + doc: { color: "#2563eb", background: "#dbeafe" }, + docx: { color: "#2563eb", background: "#dbeafe" }, + xls: { color: "#16a34a", background: "#dcfce7" }, + xlsx: { color: "#16a34a", background: "#dcfce7" }, + txt: { color: "#6b7280", background: "#f3f4f6" }, + zip: { color: "#7c3aed", background: "#ede9fe" }, + rar: { color: "#7c3aed", background: "#ede9fe" }, + mp4: { color: "#f97316", background: "#ffedd5" }, + mp3: { color: "#0d9488", background: "#ccfbf1" }, + }; + + return colorMap[extension] || getRandomColorPair(); +}; diff --git a/components/form/field/TypeUpload.tsx b/components/form/field/TypeUpload.tsx index 01254fa..4393f2f 100644 --- a/components/form/field/TypeUpload.tsx +++ b/components/form/field/TypeUpload.tsx @@ -8,6 +8,8 @@ export const TypeUpload: React.FC = ({ mode, type, disabled, + valueKey = "url", + onDelete, }) => { if (type === "multi") { return ( @@ -20,6 +22,8 @@ export const TypeUpload: React.FC = ({ fm={fm} on_change={on_change} mode={mode} + valueKey={valueKey} + onDelete={onDelete} /> ); diff --git a/components/form/field/TypeUploadMulti.tsx b/components/form/field/TypeUploadMulti.tsx index 1acbfda..a6e49b6 100644 --- a/components/form/field/TypeUploadMulti.tsx +++ b/components/form/field/TypeUploadMulti.tsx @@ -1,24 +1,24 @@ -import get from "lodash.get"; -import { Loader2, Paperclip, Trash2, Upload } from "lucide-react"; +import { Upload } from "lucide-react"; import { ChangeEvent, FC } from "react"; -import * as XLSX from "xlsx"; import { useLocal } from "@/lib/utils/use-local"; -import { siteurl } from "@/lib/utils/siteurl"; -import { FilePreview } from "./FilePreview"; import { Spinner } from "../../ui/spinner"; +import { FilePreviewBetter } from "./FilePreview"; +import { MdDelete } from "react-icons/md"; export const FieldUploadMulti: FC<{ field: any; fm: any; on_change: (e: any) => void | Promise; mode?: "upload"; -}> = ({ field, fm, on_change, mode }) => { + valueKey?: string; + onDelete?: (e: any) => any | Promise; +}> = ({ field, fm, on_change, mode, valueKey = "url", onDelete }) => { const styling = "mini"; const disabled = field?.disabled || false; let value: any = fm.data?.[field.name]; // let type_upload = const input = useLocal({ - value: 0 as any, + value: [] as any[], display: false as any, ref: null as any, drop: false as boolean, @@ -32,32 +32,6 @@ export const FieldUploadMulti: FC<{ try { file = event.target?.files?.[0]; } catch (ex) {} - const upload_single = async (file: File) => { - return { url: `/dog.jpg` }; - const formData = new FormData(); - formData.append("file", file); - const url = "/api/upload"; - const response = await fetch(url, { - method: "POST", - body: formData, - }); - - if (response.ok) { - const contentType: any = response.headers.get("content-type"); - let result; - if (contentType.includes("application/json")) { - result = await response.json(); - } else if (contentType.includes("text/plain")) { - result = await response.text(); - } else { - result = await response.blob(); - } - if (Array.isArray(result)) { - return `_file${get(result, "[0]")}`; - } - } - throw new Error("Upload Failed"); - }; if (event.target.files) { const list = [] as any[]; @@ -70,13 +44,13 @@ export const FieldUploadMulti: FC<{ list.push({ name: file.name, data: file, + [valueKey]: `${URL.createObjectURL(file)}`, }); } } - fm.data[field.name] = list; fm.render(); - on_change(fm.data?.[field.name]); + if (typeof on_change === "function") on_change(fm.data?.[field.name]); input.fase = "start"; input.render(); } @@ -85,7 +59,111 @@ export const FieldUploadMulti: FC<{ input.ref.value = null; } }; + return ( +
+ {!disabled ? ( + <> + {" "} +
+
+
+ {!disabled && ( + { + if (ref) input.ref = ref; + }} + type="file" + multiple={true} + // accept={field.prop.upload?.accept} + accept={"file/**"} + onChange={on_upload} + className={cx( + "absolute w-full h-full cursor-pointer top-0 left-0 opacity-0" + )} + /> + )} + {!disabled ? ( +
{ + if (input.ref) { + input.ref.click(); + } + }} + className="items-center flex text-base px-1 outline-none rounded cursor-pointer flex-row justify-center" + > +
+ +
+
+ Add File +
+
+ ) : ( +
+ - +
+ )} +
+
+
+ + ) : ( + <> + )} +
+ {Array.isArray(value) && value?.length ? ( + <> + {value.map((e: any, idx: number) => { + return ( +
+
+
+
+ +
+
{ + fm.data[field.name] = value.filter( + (_, i) => i !== idx + ); + fm.render(); + if (typeof on_change === "function") + on_change(fm.data?.[field.name]); + + if (typeof onDelete === "function") onDelete(e); + }} + > + +
+
+
+
+ ); + })} + + ) : ( + <> + )} +
+
+ ); return (
(string | OptItem)[] | Promise<(string | OptItem)[]>; + onSelect?: (arg: { search: string; item?: null | OptItem }) => string | false; + onChange?: (selected: string[]) => void; + unique?: boolean; + allowNew?: boolean; + className?: string; + popupClassName?: string; + localSearch?: boolean; + autoPopupWidth?: boolean; + focusOpen?: boolean; + disabled?: boolean; + mode?: "multi" | "single"; + note?: string; + disabledSearch?: boolean; + onInit?: (e: any) => void; + isBetter?: boolean; +}> = ({ + value, + fitur, + note, + options: options_fn, + onSelect, + unique, + allowNew: allow_new, + focusOpen: on_focus_open, + localSearch: local_search, + autoPopupWidth: auto_popup_width, + placeholder, + mode, + disabled, + onChange, + className, + popupClassName, + disabledSearch, + onInit, + isBetter = false, +}) => { + const maxLength = 4; + const [searchTerm, setSearchTerm] = useState(""); + const [debouncedTerm, setDebouncedTerm] = useState(""); + const local = useLocal({ + value: [] as string[], + open: false, + options: [] as OptItem[], + loaded: false, + loading: false, + selectBetter: { + all: false, + partial: [] as any[], + }, + search: { + input: "", + timeout: null as any, + searching: false, + promise: null as any, + result: null as null | OptItem[], + }, + unique: typeof unique === "undefined" ? true : unique, + allow_new: typeof allow_new === "undefined" ? false : allow_new, + on_focus_open: typeof on_focus_open === "undefined" ? true : on_focus_open, + local_search: typeof local_search === "undefined" ? true : local_search, + mode: typeof mode === "undefined" ? "multi" : mode, + auto_popup_width: + typeof auto_popup_width === "undefined" ? false : auto_popup_width, + select: null as null | OptItem, + }); + const input = useRef(null); + + let select_found = false; + let options = [...(local.search.result || local.options)]; + + const added = new Set(); + if (local.mode === "multi") { + options = options.filter((e) => { + if (!added.has(e.value)) added.add(e.value); + else return false; + if (local.select && local.select.value === e.value) select_found = true; + if (local.unique) { + if (local.value.includes(e.value)) { + return false; + } + } + return true; + }); + if (Array.isArray(value) && value?.length) { + if (!select_found) { + local.select = options[0]; + } + } + } + + useEffect(() => { + if (!value) return; + if (options.length === 0) { + loadOptions().then(() => { + if (typeof value === "object" && value) { + local.value = value; + local.render(); + } else if (typeof value === "string") { + local.value = [value]; + local.render(); + } + }); + } else { + if (typeof value === "object" && value) { + local.value = value; + local.render(); + } else { + local.value = []; + local.render(); + } + } + }, [value]); + + const select = useCallback( + (arg: { search: string; item?: null | OptItem }) => { + if (!local.allow_new) { + let found = null; + if (!arg.item) { + found = options.find((e) => e.value === arg.search); + } else { + found = options.find((e) => e.value === arg.item?.value); + } + if (!found) { + return false; + } + } + if (local.unique) { + let found = local.value.find((e) => { + return e === arg.item?.value || arg.search === e; + }); + if (found) { + return false; + } + } + + if (local.mode === "single") { + local.value = []; + } + + if (typeof onSelect === "function") { + const result = onSelect(arg); + + if (result) { + local.value.push(result); + local.render(); + + if (typeof onChange === "function") { + onChange(local.value); + } + return result; + } else { + return false; + } + } else { + let val = false as any; + if (arg.item) { + local.value.push(arg.item.value); + val = arg.item.value; + } else { + if (!arg.search) return false; + local.value.push(arg.search); + val = arg.search; + } + + if (typeof onChange === "function") { + onChange(local.value); + } + local.render(); + return val; + } + return true; + }, + [onSelect, local.value, options] + ); + + const keydown = useCallback( + (e: KeyboardEvent) => { + if (e.key === "Backspace") { + if (local.value.length > 0 && e.currentTarget.selectionStart === 0) { + local.value.pop(); + local.render(); + } + } + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + const selected = select({ + search: local.search.input, + item: local.select, + }); + + if (local.mode === "single") { + local.open = false; + } + if (typeof selected === "string") { + if (!allow_new) resetSearch(); + if (local.mode === "single") { + const item = options.find((item) => item.value === selected); + if (item) { + local.search.input = item.label; + } + } + } + + local.render(); + return; + } + if (options.length > 0) { + local.open = true; + if (e.key === "ArrowDown") { + e.preventDefault(); + e.stopPropagation(); + + const idx = options.findIndex((item) => { + if (item.value === local.select?.value) return true; + }); + if (idx >= 0) { + if (idx + 1 <= options.length - 1) { + local.select = options[idx + 1]; + } else { + local.select = options[0]; + } + } else { + local.select = options[0]; + } + local.render(); + } + if (e.key === "ArrowUp") { + e.preventDefault(); + e.stopPropagation(); + + const idx = options.findIndex((item) => { + if (item.value === local.select?.value) return true; + }); + if (idx >= 0) { + if (idx - 1 >= 0) { + local.select = options[idx - 1]; + } else { + local.select = options[options.length - 1]; + } + } else { + local.select = options[0]; + } + local.render(); + } + } + }, + [local.value, local.select, select, options, local.search.input] + ); + + const loadOptions = useCallback(async () => { + if (typeof options_fn === "function" && !local.loading) { + local.loading = true; + local.loaded = false; + local.render(); + const res = options_fn({ + search: local.search.input, + existing: options, + }); + + if (res) { + const applyOptions = (result: (string | OptItem)[]) => { + local.options = result.map((item) => { + if (typeof item === "string") return { value: item, label: item }; + return item; + }); + local.render(); + }; + + if (res instanceof Promise) { + const result = await res; + applyOptions(result); + } else { + applyOptions(res); + } + local.loaded = true; + local.loading = false; + local.render(); + } + } + }, [options_fn]); + useEffect(() => { + if (typeof onInit === "function") { + onInit({ + reload: async () => { + if (typeof options_fn === "function" && !local.loading) { + local.loading = true; + local.loaded = false; + local.render(); + const res = options_fn({ + search: local.search.input, + existing: options, + }); + + if (res) { + const applyOptions = (result: (string | OptItem)[]) => { + local.options = result.map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.render(); + }; + + if (res instanceof Promise) { + const result = await res; + applyOptions(result); + } else { + applyOptions(res); + } + local.loaded = true; + local.loading = false; + local.render(); + } + } + }, + }); + } + }, []); + // Debounce effect + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedTerm(searchTerm); // Update debounced term after 1 second + }, 100); + + return () => clearTimeout(timer); // Clear timeout if user types again + }, [searchTerm]); + + // Function to handle search + useEffect(() => { + if (debouncedTerm) { + performSearch(debouncedTerm); + } + }, [debouncedTerm]); + + const performSearch = (value: any) => { + if (typeof onSelect === "function") { + const result = onSelect({ + search: value, + item: { + label: value, + value: value, + }, + }); + + if (result) { + local.value.push(result); + local.render(); + + if (typeof onChange === "function") { + onChange(local.value); + } + return result; + } else { + return false; + } + } + // Lakukan pencarian, panggil API, atau filter data di sini + }; + const resetSearch = () => { + local.search.searching = false; + local.search.input = ""; + local.search.promise = null; + local.search.result = null; + local.select = null; + clearTimeout(local.search.timeout); + }; + + if (local.mode === "single" && local.value.length > 1) { + local.value = [local.value.pop() || ""]; + } + + if (local.value.length === 0) { + if (local.mode === "single") { + if (!local.open && !allow_new) { + local.select = null; + + local.search.input = ""; + } + } + } + + const valueLabel = uniqBy( + local.value?.map((value) => { + if (local.mode === "single") { + const item = options.find((item) => item.value === value); + + if (!local.open && !allow_new) { + local.select = item || null; + + local.search.input = item?.tag || item?.label || ""; + } + return item; + } + + const item = local.options.find((e) => e.value === value); + return item; + }), + "value" + ); + let inputval = local.search.input; + + if (!local.open && local.mode === "single" && local.value?.length > 0) { + const found = options.find((e) => e.value === local.value[0]); + if (found) { + inputval = found.tag || found.label; + } else { + inputval = local.value[0]; + } + } + useEffect(() => { + if (allow_new && local.open) { + local.search.input = local.value[0]; + local.render(); + } + }, [local.open]); + + return ( +
+
{ + if (!disabled) input.current?.focus(); + }} + > + {local.mode === "multi" ? ( +
+ {valueLabel.map((e, idx) => { + return ( + { + if (!disabled) { + ev.stopPropagation(); + ev.preventDefault(); + local.value = local.value.filter( + (val) => e?.value !== val + ); + local.render(); + input.current?.focus(); + + if (typeof onChange === "function") { + onChange(local.value); + } + } + }} + > +
+ {e?.tag || e?.label || <> } +
+ {!disabled && } +
+ ); + })} +
+ ) : ( + <> + )} + { + if (!open) { + local.select = null; + } + local.open = open; + local.render(); + + if (!open) { + resetSearch(); + } + }} + onRemove={(data) => { + local.value = local.value.filter((val) => data?.value !== val); + local.render(); + input.current?.focus(); + + if (typeof onChange === "function") { + onChange(local.value); + } + }} + onSelectAll={(data: boolean) => { + local.value = data ? options.map((e) => e?.value) : []; + local.render(); + input.current?.focus(); + if (typeof onChange === "function") { + onChange(local.value); + } + }} + init={local} + isBetter={isBetter} + loading={local.loading} + showEmpty={!allow_new} + className={popupClassName} + open={local.open} + options={options} + searching={local.search.searching} + searchText={local.search.input} + onSearch={async (e) => { + const val = e.currentTarget.value; + if (!local.open) { + local.open = true; + } + + local.search.input = val; + local.render(); + + if (local.search.promise) { + await local.search.promise; + } + + local.search.searching = true; + local.render(); + if (allow_new) { + setSearchTerm(val); + } + if (local.search.searching) { + if (local.local_search) { + if (!local.loaded) { + await loadOptions(); + } + const search = local.search.input.toLowerCase(); + if (search) { + local.search.result = options.filter((e) => + e.label.toLowerCase().includes(search) + ); + + if ( + local.search.result.length > 0 && + !local.search.result.find( + (e) => e.value === local.select?.value + ) + ) { + } + } else { + local.search.result = null; + } + local.search.searching = false; + local.render(); + } else { + clearTimeout(local.search.timeout); + local.search.timeout = setTimeout(async () => { + const result = options_fn?.({ + search: local.search.input, + existing: options, + }); + if (result) { + if (result instanceof Promise) { + local.search.promise = result; + local.search.result = (await result).map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + local.search.promise = null; + } else { + local.search.result = result.map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + } + + if ( + local.search.result.length > 0 && + !local.search.result.find( + (e) => e.value === local.select?.value + ) + ) { + } + + local.render(); + } + }, 100); + } + } + }} + onSelect={(value) => { + if (!isBetter) local.open = false; + resetSearch(); + const item = options.find((item) => item.value === value); + if (item) { + let search = local.search.input; + if (local.mode === "single") { + local.search.input = item.tag || item.label; + } else { + local.search.input = ""; + } + + select({ + search, + item, + }); + } + + local.render(); + }} + width={ + local.auto_popup_width ? input.current?.offsetWidth : undefined + } + isMulti={local.mode === "multi"} + selected={({ item, options, idx }) => { + // console.log(local.select); + if (isBetter) { + const val = local.value?.length ? local.value : []; + let isSelect = options.find((e) => { + return ( + e?.value === item?.value && + val.find((ex) => ex === item?.value) + ); + }); + return isSelect ? true : false; + } else if (item.value === local.select?.value) { + return true; + } + + return false; + }} + > +
{ + e.stopPropagation(); + if (!disabled) { + if (!local.open) { + if (local.on_focus_open) { + loadOptions(); + local.open = true; + local.render(); + // if (allow_new) { + // local.search.input = inputval; + // local.render(); + // } + } + } + if (local.mode === "single") { + if (input && input.current) input.current.select(); + } + } + }} + > + {isBetter ? ( +
+
+
+ +
+
+ ) : ( + { + const val = e.currentTarget.value; + if (!local.open) { + local.open = true; + } + + local.search.input = val; + local.render(); + + if (local.search.promise) { + await local.search.promise; + } + + local.search.searching = true; + local.render(); + if (allow_new) { + setSearchTerm(val); + } + if (local.search.searching) { + if (local.local_search) { + if (!local.loaded) { + await loadOptions(); + } + const search = local.search.input.toLowerCase(); + if (search) { + local.search.result = options.filter((e) => + e.label.toLowerCase().includes(search) + ); + + if ( + local.search.result.length > 0 && + !local.search.result.find( + (e) => e.value === local.select?.value + ) + ) { + local.select = local.search.result[0]; + } + } else { + local.search.result = null; + } + local.search.searching = false; + local.render(); + } else { + clearTimeout(local.search.timeout); + local.search.timeout = setTimeout(async () => { + const result = options_fn?.({ + search: local.search.input, + existing: options, + }); + if (result) { + if (result instanceof Promise) { + local.search.promise = result; + local.search.result = (await result).map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + local.search.promise = null; + } else { + local.search.result = result.map((item) => { + if (typeof item === "string") + return { value: item, label: item }; + return item; + }); + local.search.searching = false; + } + + if ( + local.search.result.length > 0 && + !local.search.result.find( + (e) => e.value === local.select?.value + ) + ) { + local.select = local.search.result[0]; + } + + local.render(); + } + }, 100); + } + } + }} + disabled={!disabled ? disabledSearch : disabled} + spellCheck={false} + className={cx( + "text-black flex h-9 w-full border-input bg-transparent px-3 py-1 text-base border-none shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground md:text-sm focus:outline-none focus:ring-0", + local.mode === "single" ? "cursor-pointer" : "" + )} + style={{ + pointerEvents: disabledSearch ? "none" : "auto", // Mencegah input menangkap klik saat disabled + }} + onKeyDown={keydown} + /> + )} +
+
+
+ + {local.mode === "single" && fitur !== "search-add" && ( + <> +
{ + if (!disabled) { + local.value = []; + local.render(); + if (typeof onChange === "function") onChange(local.value); + } + }} + > + {inputval ? : } +
+ + )} +
+ ); +}; diff --git a/components/tablelist/List.tsx b/components/tablelist/List.tsx index ad45501..621d741 100644 --- a/components/tablelist/List.tsx +++ b/components/tablelist/List.tsx @@ -41,6 +41,7 @@ export const ListBetter: React.FC = ({ paging: 1, maxPage: 1, count: 0 as any, + ready: false, addRow: (row: any) => { setData((prev) => [...prev, row]); local.data.push(row); @@ -83,10 +84,13 @@ export const ListBetter: React.FC = ({ {"Loading..."} ); - + local.ready = false; + local.render(); if (typeof onCount === "function") { const res = await onCount(); local.count = res; + local.maxPage = Math.ceil(res / take); + local.paging = 1; local.render(); } if (Array.isArray(onLoad)) { @@ -108,6 +112,8 @@ export const ListBetter: React.FC = ({ toast.dismiss(); }, 100); } + local.ready = true; + local.render(); }, reload: async () => { toast.info( @@ -176,9 +182,10 @@ export const ListBetter: React.FC = ({ {"Loading..."} ); + local.ready = false; + local.render(); if (typeof onCount === "function") { const res = await onCount(); - console.log(res, take, Math.ceil(res / take)); setMaxPage(Math.ceil(res / take)); local.maxPage = Math.ceil(res / take); local.count = res; @@ -213,6 +220,8 @@ export const ListBetter: React.FC = ({ if (typeof onInit === "function") { onInit(local); } + local.ready = true; + local.render(); setTimeout(() => { toast.dismiss(); }, 100); @@ -246,25 +255,33 @@ export const ListBetter: React.FC = ({ className="w-full h-full flex flex-col gap-y-4 p-4" reload={reload} > -
- {Array.isArray(local.data) && local.data?.length ? ( - local.data?.map((e, idx) => { - return ( -
- {typeof content === "function" - ? content({ item: e, idx, tbl: local }) - : content} -
- ); - }) - ) : ( - <> - )} -
+ {!local.ready ? ( + <> +
+
+
+ + ) : ( +
+ {Array.isArray(local.data) && local.data?.length ? ( + local.data?.map((e, idx) => { + return ( +
+ {typeof content === "function" + ? content({ item: e, idx, tbl: local }) + : content} +
+ ); + }) + ) : ( + <> + )} +
+ )}
diff --git a/components/ui/Progress.tsx b/components/ui/Progress.tsx index 0325243..6a30b88 100644 --- a/components/ui/Progress.tsx +++ b/components/ui/Progress.tsx @@ -6,8 +6,10 @@ import { cn } from "@/lib/utils"; const Progress = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, value, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + classNameIndicator?: any; // Trigger to force height update + } +>(({ className, value, classNameIndicator, ...props }, ref) => ( diff --git a/utils/apix.ts b/utils/apix.ts index b4da9de..0689cb7 100644 --- a/utils/apix.ts +++ b/utils/apix.ts @@ -2,7 +2,7 @@ import get from "lodash.get"; import api from "./axios"; type apixType = { - port: "portal" | "recruitment" | "mpp" | "public"; + port: "portal" | "recruitment" | "mpp" | "public" | "onboarding"; path: string; method?: "get" | "delete" | "post" | "put"; data?: any; @@ -36,6 +36,8 @@ export const apix = async ({ ? process.env.NEXT_PUBLIC_API_RECRUITMENT : port === "mpp" ? process.env.NEXT_PUBLIC_API_MPP + : port === "onboarding" + ? process.env.NEXT_PUBLIC_API_ONBOARDING : port === "public" ? process.env.NEXT_PUBLIC_BASE_URL : "" @@ -49,10 +51,20 @@ export const apix = async ({ const requestData = type === "form" && data ? Object.entries(data as any).reduce((formData, [key, value]) => { - formData.append( - key.includes("certificate") ? key : key.replace(/\[\d+\]/, ""), - value as any - ); + if (Array.isArray(value) && value?.length) { + value.map((item: any) => { + formData.append(key, item as any); + }); + } else if (value instanceof FormData) { + value.forEach((value, key) => { + formData.append(key, value); + }); + } else { + formData.append( + key.includes("certificate") ? key : key.replace(/\[\d+\]/, ""), + value as any + ); + } return formData; }, new FormData()) : data; diff --git a/utils/convetForm.ts b/utils/convetForm.ts new file mode 100644 index 0000000..d7ad8d7 --- /dev/null +++ b/utils/convetForm.ts @@ -0,0 +1,14 @@ +export const convertForm = ({ + data, + task, +}: { + data: any[]; + task: (item: any, form: any) => void; +}) => { + const form = new FormData(); + if (Array.isArray(data) && data?.length) { + data.map((item: any) => { + task(item, form); + }); + } +};