lib
This commit is contained in:
parent
372bf8bd52
commit
c3dbeb8c33
|
|
@ -79,7 +79,8 @@ export const Field: React.FC<any> = ({
|
||||||
? "flex flex-row rounded-md flex-grow border-red-500 border items-center"
|
? "flex flex-row rounded-md flex-grow border-red-500 border items-center"
|
||||||
: "flex flex-row rounded-md flex-grow items-center",
|
: "flex flex-row rounded-md flex-grow items-center",
|
||||||
is_disable ? "bg-gray-100" : "",
|
is_disable ? "bg-gray-100" : "",
|
||||||
"relative"
|
"relative",
|
||||||
|
""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{before && (
|
{before && (
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ function generateRandomColor(str: string): string {
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
const getFileName = (url: string) => {
|
const getFileName = (url: string) => {
|
||||||
if (url.startsWith("[")) {
|
if (url && url.startsWith("[")) {
|
||||||
try {
|
try {
|
||||||
const list = JSON.parse(url);
|
const list = JSON.parse(url);
|
||||||
if (list.length === 0) return "Empty";
|
if (list.length === 0) return "Empty";
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@ import { useLocal } from "@/lib/utils/use-local";
|
||||||
import Datepicker from "../../ui/Datepicker";
|
import Datepicker from "../../ui/Datepicker";
|
||||||
import { Input } from "../../ui/input";
|
import { Input } from "../../ui/input";
|
||||||
import { Textarea } from "../../ui/text-area";
|
import { Textarea } from "../../ui/text-area";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import tinycolor from "tinycolor2";
|
import tinycolor from "tinycolor2";
|
||||||
import { FieldColorPicker } from "../../ui/FieldColorPopover";
|
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<any> = ({
|
export const TypeInput: React.FC<any> = ({
|
||||||
name,
|
name,
|
||||||
|
|
@ -17,7 +20,15 @@ export const TypeInput: React.FC<any> = ({
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [hover, setHover] = useState(0); // State untuk menyimpan nilai hover
|
||||||
|
|
||||||
let value: any = fm.data?.[name] || "";
|
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({
|
const input = useLocal({
|
||||||
value: 0 as any,
|
value: 0 as any,
|
||||||
ref: null as any,
|
ref: null as any,
|
||||||
|
|
@ -35,6 +46,8 @@ export const TypeInput: React.FC<any> = ({
|
||||||
const convertColor = tinycolor(meta.inputValue);
|
const convertColor = tinycolor(meta.inputValue);
|
||||||
meta.rgbValue = convertColor.toRgbString();
|
meta.rgbValue = convertColor.toRgbString();
|
||||||
meta.render();
|
meta.render();
|
||||||
|
} else {
|
||||||
|
setRating(value ? value - 1 : value);
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -85,6 +98,59 @@ export const TypeInput: React.FC<any> = ({
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "rating":
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<Rating
|
||||||
|
rating={getNumber(fm.data?.[name])}
|
||||||
|
totalStars={5}
|
||||||
|
size={24}
|
||||||
|
variant="yellow"
|
||||||
|
disabled={disabled}
|
||||||
|
className="h-1"
|
||||||
|
showText={false}
|
||||||
|
onRatingChange={(e) => {
|
||||||
|
fm.data[name] = getNumber(e);
|
||||||
|
fm.render();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex">
|
||||||
|
{Array.from({ length: 5 }, (_, index) => index + 1).map(
|
||||||
|
(number) => (
|
||||||
|
<button
|
||||||
|
key={number}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClick(number);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHover(number)} // Set nilai hover saat mouse masuk
|
||||||
|
onMouseLeave={() => setHover(number)} // Reset nilai hover saat mouse keluar
|
||||||
|
className={cx(
|
||||||
|
"focus:outline-none px-0.5",
|
||||||
|
disabled ? "" : "transition-transform duration-200"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
transform:
|
||||||
|
hover === number && !disabled ? "scale(1.2)" : "scale(1)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hover >= number || rating >= number ? (
|
||||||
|
<FaStar className="text-yellow-400" /> // Star yang diisi (fill)
|
||||||
|
) : (
|
||||||
|
<FaRegStar className="text-gray-400" /> // Star yang kosong (outline)
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
break;
|
||||||
case "color":
|
case "color":
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
|
|
|
||||||
|
|
@ -770,7 +770,7 @@ export const TypeRichText: React.FC<any> = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cx(
|
className={cx(
|
||||||
"flex flex-col relative bg-white border border-gray-300 rounded-md w-full",
|
"flex flex-col relative bg-white border border-gray-300 rounded-md w-full richtext-field",
|
||||||
css`
|
css`
|
||||||
.tiptap h1 {
|
.tiptap h1 {
|
||||||
font-size: 1.4rem !important;
|
font-size: 1.4rem !important;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Star } from "lucide-react";
|
||||||
|
import { cn } from "@/lib/utils/utils";
|
||||||
|
|
||||||
|
const ratingVariants = {
|
||||||
|
default: {
|
||||||
|
star: "text-foreground",
|
||||||
|
emptyStar: "text-muted-foreground",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
star: "text-red-500",
|
||||||
|
emptyStar: "text-red-200",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
star: "text-yellow-300",
|
||||||
|
emptyStar: "text-yellow-300",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RatingProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
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 = <Star />,
|
||||||
|
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<number | null>(null);
|
||||||
|
const [currentRating, setCurrentRating] = useState(initialRating);
|
||||||
|
const [isHovering, setIsHovering] = useState(false);
|
||||||
|
|
||||||
|
const handleMouseEnter = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
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<HTMLDivElement>) => {
|
||||||
|
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 ? (
|
||||||
|
<PartialStar
|
||||||
|
fillPercentage={displayRating % 1}
|
||||||
|
size={size}
|
||||||
|
className={cn(ratingVariants[variant].star)}
|
||||||
|
Icon={Icon}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("flex w-fit flex-col gap-2", {
|
||||||
|
"pointer-events-none": disabled,
|
||||||
|
})}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="flex items-center" onMouseEnter={handleMouseEnter}>
|
||||||
|
{[...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,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showText && (
|
||||||
|
<span className="text-xs text-muted-foreground font-semibold">
|
||||||
|
Current Rating: {`${currentRating}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PartialStarProps {
|
||||||
|
fillPercentage: number;
|
||||||
|
size: number;
|
||||||
|
className?: string;
|
||||||
|
Icon: React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PartialStar = ({
|
||||||
|
fillPercentage,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
Icon,
|
||||||
|
}: PartialStarProps) => {
|
||||||
|
return (
|
||||||
|
<div style={{ position: "relative", display: "inline-block" }}>
|
||||||
|
{React.cloneElement(Icon, {
|
||||||
|
size,
|
||||||
|
className: cn("fill-transparent", className),
|
||||||
|
})}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
overflow: "hidden",
|
||||||
|
width: `${fillPercentage * 100}%`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{React.cloneElement(Icon, {
|
||||||
|
size,
|
||||||
|
className: cn("fill-current", className),
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -42,7 +42,7 @@ export const apix = async ({
|
||||||
const requestData =
|
const requestData =
|
||||||
type === "form" && data
|
type === "form" && data
|
||||||
? Object.entries(data as any).reduce((formData, [key, value]) => {
|
? 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;
|
return formData;
|
||||||
}, new FormData())
|
}, new FormData())
|
||||||
: data;
|
: data;
|
||||||
|
|
@ -105,3 +105,13 @@ export const apix = async ({
|
||||||
throw error;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import dotenv from 'dotenv';
|
import dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
export const siteurl = (param: string) => {
|
export const siteurl = (param: string) => {
|
||||||
return `${process.env.NEXT_PUBLIC_BASE_URL + param}`
|
if (param.startsWith("http")) return param;
|
||||||
}
|
return `${process.env.NEXT_PUBLIC_BASE_URL + param}`;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue