diff --git a/components/form/Field.tsx b/components/form/Field.tsx index 9036312..87d706b 100644 --- a/components/form/Field.tsx +++ b/components/form/Field.tsx @@ -79,7 +79,8 @@ export const Field: React.FC = ({ ? "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" + "relative", + "" )} > {before && ( diff --git a/components/form/field/FilePreview.tsx b/components/form/field/FilePreview.tsx index 19f9af9..8d836d2 100644 --- a/components/form/field/FilePreview.tsx +++ b/components/form/field/FilePreview.tsx @@ -269,7 +269,7 @@ function generateRandomColor(str: string): string { return color; } const getFileName = (url: string) => { - if (url.startsWith("[")) { + if (url && url.startsWith("[")) { try { const list = JSON.parse(url); if (list.length === 0) return "Empty"; diff --git a/components/form/field/TypeInput.tsx b/components/form/field/TypeInput.tsx index 9898521..9e806d6 100644 --- a/components/form/field/TypeInput.tsx +++ b/components/form/field/TypeInput.tsx @@ -2,9 +2,12 @@ import { useLocal } from "@/lib/utils/use-local"; import Datepicker from "../../ui/Datepicker"; import { Input } from "../../ui/input"; import { Textarea } from "../../ui/text-area"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import tinycolor from "tinycolor2"; import { FieldColorPicker } from "../../ui/FieldColorPopover"; +import { FaRegStar, FaStar } from "react-icons/fa6"; +import { Rating } from "../../ui/ratings"; +import { getNumber } from "@/lib/utils/getNumber"; export const TypeInput: React.FC = ({ name, @@ -17,7 +20,15 @@ export const TypeInput: React.FC = ({ onChange, className, }) => { + const [hover, setHover] = useState(0); // State untuk menyimpan nilai hover + let value: any = fm.data?.[name] || ""; + const [rating, setRating] = useState(value); // State untuk menyimpan nilai rating + const handleClick = (index: number) => { + setRating(index); // Update nilai rating + fm.data[name] = rating + 1; + fm.render(); + }; const input = useLocal({ value: 0 as any, ref: null as any, @@ -35,6 +46,8 @@ export const TypeInput: React.FC = ({ const convertColor = tinycolor(meta.inputValue); meta.rgbValue = convertColor.toRgbString(); meta.render(); + } else { + setRating(value ? value - 1 : value); } }, [value]); useEffect(() => { @@ -85,6 +98,59 @@ export const TypeInput: React.FC = ({ ); break; + case "rating": + return ( +
+ { + fm.data[name] = getNumber(e); + fm.render(); + }} + /> +
+ ); + return ( + <> +
+ {Array.from({ length: 5 }, (_, index) => index + 1).map( + (number) => ( + + ) + )} +
+ + ); + break; case "color": return (
diff --git a/components/form/field/TypeRichText.tsx b/components/form/field/TypeRichText.tsx index 2f9478f..16788f7 100644 --- a/components/form/field/TypeRichText.tsx +++ b/components/form/field/TypeRichText.tsx @@ -770,7 +770,7 @@ export const TypeRichText: React.FC = ({ return (
{ + rating: number; + totalStars?: number; + size?: number; + fill?: boolean; + Icon?: React.ReactElement; + variant?: keyof typeof ratingVariants; + onRatingChange?: (rating: number) => void; + showText?: boolean; // Add showText prop + disabled?: boolean; +} + +export const Rating = ({ + rating: initialRating, + totalStars = 5, + size = 20, + fill = true, + Icon = , + variant = "default", + onRatingChange, + showText = true, // Default to true if disabled prop is not provided + disabled = false, // Default to false if disabled prop is not provided + ...props +}: RatingProps) => { + const [hoverRating, setHoverRating] = useState(null); + const [currentRating, setCurrentRating] = useState(initialRating); + const [isHovering, setIsHovering] = useState(false); + + const handleMouseEnter = (event: React.MouseEvent) => { + if (!disabled) { + setIsHovering(true); + const starIndex = parseInt( + (event.currentTarget as HTMLDivElement).dataset.starIndex || "0" + ); + setHoverRating(starIndex); + } + }; + + const handleMouseLeave = () => { + setIsHovering(false); + setHoverRating(null); + }; + + const handleClick = (event: React.MouseEvent) => { + if (!disabled) { + const starIndex = parseInt( + (event.currentTarget as HTMLDivElement).dataset.starIndex || "0" + ); + setCurrentRating(starIndex); + setHoverRating(null); + if (onRatingChange) { + onRatingChange(starIndex); + } + } + }; + + const displayRating = disabled ? initialRating : hoverRating ?? currentRating; + const fullStars = Math.floor(displayRating); + const partialStar = + displayRating % 1 > 0 ? ( + + ) : null; + + return ( +
+
+ {[...Array(fullStars)].map((_, i) => + React.cloneElement(Icon, { + key: i, + size, + className: cn( + fill ? "fill-current stroke-1" : "fill-transparent", + ratingVariants[variant].star + ), + onClick: handleClick, + onMouseEnter: handleMouseEnter, + "data-star-index": i + 1, + }) + )} + {partialStar} + {[ + ...Array(Math.max(0, totalStars - fullStars - (partialStar ? 1 : 0))), + ].map((_, i) => + React.cloneElement(Icon, { + key: i + fullStars + 1, + size, + className: cn("stroke-1", ratingVariants[variant].emptyStar), + onClick: handleClick, + onMouseEnter: handleMouseEnter, + "data-star-index": i + fullStars + 1, + }) + )} +
+ {showText && ( + + Current Rating: {`${currentRating}`} + + )} +
+ ); +}; + +interface PartialStarProps { + fillPercentage: number; + size: number; + className?: string; + Icon: React.ReactElement; +} + +const PartialStar = ({ + fillPercentage, + size, + className, + Icon, +}: PartialStarProps) => { + return ( +
+ {React.cloneElement(Icon, { + size, + className: cn("fill-transparent", className), + })} +
+ {React.cloneElement(Icon, { + size, + className: cn("fill-current", className), + })} +
+
+ ); +}; diff --git a/utils/apix.ts b/utils/apix.ts index 71b9f9f..c612ad6 100644 --- a/utils/apix.ts +++ b/utils/apix.ts @@ -42,7 +42,7 @@ export const apix = async ({ const requestData = type === "form" && data ? Object.entries(data as any).reduce((formData, [key, value]) => { - formData.append(key, value as any); + formData.append(key.replace(/\[\d+\]/, ""), value as any); return formData; }, new FormData()) : data; @@ -105,3 +105,13 @@ export const apix = async ({ throw error; } }; +function removeIndexFromKey(obj: any) { + let result = {} as any; + + for (const [key, value] of Object.entries(obj)) { + const newKey = key.replace(/\[\d+\]/, ""); // Hapus indeks [n] dari key + result[newKey] = value; + } + + return result; +} diff --git a/utils/flattenObject.ts b/utils/flattenObject.ts new file mode 100644 index 0000000..5d7e335 --- /dev/null +++ b/utils/flattenObject.ts @@ -0,0 +1,41 @@ +import { normalDate } from "./date"; + +export const flattenObject = ( + obj: any, + parentKey: string = "", + result: any = {}, + idx: any = "" +): any => { + for (const [key, value] of Object.entries(obj)) { + // Buat key baru + const newKey = parentKey + ? `${parentKey}.${key}${idx ? `[${idx}]` : ``}` + : key; + console.log({ newKey }); + if (Array.isArray(value)) { + // Jika value adalah array, loop dan tambahkan indeks + value.forEach((item, index) => { + if (typeof item === "object" && item !== null) { + // Rekursi jika elemen adalah object + flattenObject(item, `${newKey}`, result, `${index}`); + } else { + console.log(`${newKey}.${key}[${index}]`); + // Simpan value langsung jika elemen bukan object + result[`${newKey}.${key}[${index}]`] = item; + } + }); + } else { + // Simpan value langsung jika bukan array atau object + if (["end_date", "birth_date"].includes(key)) { + result[newKey] = normalDate(value as any); + } else { + if (["ktp", "certificate", "curriculum_vitae"].includes(key)) { + if (typeof value !== "string" && value) result[newKey] = value; + } else { + result[newKey] = value; + } + } + } + } + return result; +}; diff --git a/utils/siteurl.ts b/utils/siteurl.ts index f3d66a3..ac310ce 100644 --- a/utils/siteurl.ts +++ b/utils/siteurl.ts @@ -1,5 +1,6 @@ -import dotenv from 'dotenv'; +import dotenv from "dotenv"; dotenv.config(); export const siteurl = (param: string) => { - return `${process.env.NEXT_PUBLIC_BASE_URL + param}` -} \ No newline at end of file + if (param.startsWith("http")) return param; + return `${process.env.NEXT_PUBLIC_BASE_URL + param}`; +};