feat: add Up and Down SVG components and improve dropdown menu label

This commit is contained in:
faisolavolut 2025-02-24 23:45:27 +07:00
parent b68a99cb22
commit af386a2b07
12 changed files with 251 additions and 86 deletions

View File

@ -188,6 +188,7 @@ export function Popover({
classNameTrigger,
arrow,
popoverClassName,
onMouseDown,
...restOptions
}: {
root?: HTMLElement;
@ -197,6 +198,7 @@ export function Popover({
content?: React.ReactNode;
popoverClassName?: string;
arrow?: boolean;
onMouseDown?: (event: any) => void;
} & PopoverOptions) {
const popover = usePopover({ modal, ...restOptions });
@ -231,6 +233,7 @@ export function Popover({
`
)
)}
onMouseDown={onMouseDown}
>
{_content}
{(typeof arrow === "undefined" || arrow) && <PopoverArrow />}

View File

@ -2,9 +2,10 @@ import { AsyncPaginate } from "react-select-async-paginate";
import { components } from "react-select";
import { useLocal } from "@/lib/utils/use-local";
import { empty } from "@/lib/utils/isStringEmpty";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import debounce from "lodash.debounce";
import get from "lodash.get";
import { Popover } from "../../Popover/Popover";
export const TypeAsyncDropdown: React.FC<any> = ({
name,
@ -23,13 +24,28 @@ export const TypeAsyncDropdown: React.FC<any> = ({
search = "api",
required = false,
}) => {
const [cacheUniq, setCacheUniq] = useState(Date.now());
const [open, setOpen] = useState(false as boolean);
const [refreshKey, setRefreshKey] = useState(Date.now());
const selectRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState<number>(0);
const getValue =
typeof onValue === "string" ? (e: any) => get(e, onValue) : onValue;
typeof onValue === "string"
? (e: any) => {
if (typeof e !== "object" && !Array.isArray(e)) {
return e;
}
return get(e, onValue);
}
: onValue;
const getLabel =
typeof onLabel === "string" ? (e: any) => get(e, onLabel) : onLabel;
typeof onLabel === "string"
? (e: any) => {
if (typeof e !== "object" && !Array.isArray(e)) {
return e;
}
return get(e, onLabel);
}
: onLabel;
let placeholderField =
mode === "multi"
? placeholder || `Add ${label}`
@ -227,82 +243,125 @@ export const TypeAsyncDropdown: React.FC<any> = ({
label: getLabel(value),
};
}
const CustomMenu = (props: any) => {
return (
<Popover
onMouseDown={(e) => {
e.stopPropagation();
e.preventDefault();
}}
classNameTrigger={""}
arrow={false}
className="rounded-md"
onOpenChange={(open: any) => {}}
open={true}
content={
<div
className={cx(
"flex flex-col flex-grow",
css`
width: ${width}px;
`
)}
>
{props.children}
</div>
}
>
<></>
</Popover>
);
};
useEffect(() => {
if (selectRef.current) {
setWidth(selectRef.current.offsetWidth);
}
}, [selectRef]);
return (
<AsyncPaginate
// menuIsOpen={true}
key={refreshKey}
placeholder={disabled ? "" : placeholderField}
isDisabled={disabled}
className={cx(
"rounded-md border-none text-sm",
css`
[role="listbox"] {
padding: 0px !important;
z-index: 5;
<div ref={selectRef} className="w-full">
<AsyncPaginate
menuIsOpen={open}
key={refreshKey}
placeholder={disabled ? "" : placeholderField}
isDisabled={disabled}
className={cx(
"rounded-md border-none text-sm",
css`
[role="listbox"] {
padding: 0px !important;
z-index: 5;
}
input:focus {
outline: 0px !important;
border: 0px !important;
outline-offset: 0px !important;
--tw-ring-color: transparent !important;
}
.css-13cymwt-control {
border-color: transparent;
border-width: 0px;
border-radius: 6px;
}
.css-t3ipsp-control {
border-color: transparent;
border-width: 0px;
box-shadow: none;
border-radius: 6px;
}
> :nth-child(4) {
z-index: 4 !important;
}
`,
disabled
? css`
> div {
border-width: 0px !important;
background: transparent !important;
}
> div > div:last-child {
display: none !important;
}
> div > div:first-child > div {
color: black !important;
}
`
: ``
)}
isClearable={clearable}
onMenuOpen={() => {
setOpen(true);
}}
onMenuClose={() => {
setOpen(false);
}}
// closeMenuOnSelect={mode === "dropdown" ? true : false}
closeMenuOnSelect={false}
getOptionValue={(item) => item.value}
getOptionLabel={(item) => item.label}
value={value}
components={{ MultiValue, Option, Menu: CustomMenu }}
loadOptions={loadOptions}
isSearchable={true}
isMulti={mode === "multi"}
onChange={(e) => {
setOpen(mode === "dropdown" ? false : true);
if (target) {
fm.data[target] = getValue(e);
}
input:focus {
outline: 0px !important;
border: 0px !important;
outline-offset: 0px !important;
--tw-ring-color: transparent !important;
if (mode === "dropdown" && !target) {
fm.data[name] = getValue(e);
} else {
fm.data[name] = e;
}
.css-13cymwt-control {
border-color: transparent;
border-width: 0px;
border-radius: 6px;
fm.render();
if (typeof onChange === "function") {
onChange({ data: e });
}
.css-t3ipsp-control {
border-color: transparent;
border-width: 0px;
box-shadow: none;
border-radius: 6px;
}
> :nth-child(4) {
z-index: 4 !important;
}
`,
disabled
? css`
> div {
border-width: 0px !important;
background: transparent !important;
}
> div > div:last-child {
display: none !important;
}
> div > div:first-child > div {
color: black !important;
}
`
: ``
)}
isClearable={clearable}
closeMenuOnSelect={mode === "dropdown" ? true : false}
getOptionValue={(item) => item.value}
getOptionLabel={(item) => item.label}
value={value}
components={{ MultiValue, Option }}
loadOptions={loadOptions}
isSearchable={true}
isMulti={mode === "multi"}
onChange={(e) => {
if (target) {
fm.data[target] = getValue(e);
}
if (mode === "dropdown" && !target) {
fm.data[name] = getValue(e);
} else {
fm.data[name] = e;
}
fm.render();
if (typeof onChange === "function") {
onChange({ data: e });
}
}}
additional={{
page: 1,
}}
/>
}}
additional={{
page: 1,
}}
/>
</div>
);
};

View File

@ -10,7 +10,7 @@ import { Rating } from "../../ui/ratings";
import { getNumber } from "@/lib/utils/getNumber";
import MaskedInput from "../../ui/MaskedInput";
import { cn } from "@/lib/utils";
import { getStatusLabel } from "@/constants/status-mpp";
import { getLabel } from "@/lib/utils/getLabel";
export const TypeInput: React.FC<any> = ({
name,
@ -332,7 +332,7 @@ export const TypeInput: React.FC<any> = ({
disabled={disabled}
required={required}
placeholder={placeholder || ""}
value={getStatusLabel(value)}
value={getLabel(value)}
type={!type ? "text" : type_field}
onChange={(ev) => {
fm.data[name] = ev.currentTarget.value;

View File

@ -286,7 +286,13 @@ const Input: React.FC<Props> = (e: Props) => {
return (
<>
{disabled ? (
<div className={"flex h-9 w-full rounded-md border border-gray-200 border-input bg-gray-100 items-center px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"}>{inputText ? inputText : "-"}</div>
<div
className={
"flex h-9 w-full rounded-md border-input bg-gray-100 items-center px-3 py-1 text-base transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
}
>
{inputText ? inputText : "-"}
</div>
) : (
<input
ref={inputRef}

View File

@ -1,3 +1,4 @@
import { cn } from "@/lib/utils";
import { useLocal } from "@/lib/utils/use-local";
import { useEffect } from "react";
import { v4 as uuidv4 } from "uuid";
@ -40,11 +41,17 @@ export const PinterestLayout: React.FC<{
<>
<div className="flex flex-grow flex-1 flex-col w-full h-full">
<div
className={cx(
className={cn(
`grid gap-${gap}`,
css`
grid-template-columns: repeat(${col}, minmax(0, 1fr));
`
@media (max-width: 768px) {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 769px) {
grid-template-columns: repeat(${col}, minmax(0, 1fr));
}
`,
""
)}
>
{local.data.map((el, idx) => {

View File

@ -27,6 +27,8 @@ const buttonVariants = cva(
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",
clean:
"border-none bg-background shadow-none hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",

View File

@ -47,7 +47,7 @@ const DialogContent = React.forwardRef<
{typeof onClick === "function" ? (
<div
onClick={onClick}
className="cursor-pointer absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
className="dialog-close cursor-pointer absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>

View File

@ -232,7 +232,7 @@ const DropdownHamburgerBetter: React.FC<{
setOpen(!open);
}}
>
More Actions
Actions
{open ? <IoIosArrowDown /> : <IoIosArrowUp />}
</ButtonBetter>
</DropdownMenuTrigger>

View File

@ -9,6 +9,7 @@ export const userToken = async () => {
if (user) {
try {
await userRoleMe();
return true;
} catch (ex: any) {
const error = get(ex, "response.data.meta.message") || ex.message;
if (error === "Request failed with status code 401") {
@ -26,6 +27,36 @@ export const userToken = async () => {
w.user = JSON.parse(user);
}
return true;
} else {
try {
let user = await apix({
port: "portal",
value: "data.data",
path: "/api/users/me",
});
console.log(user);
if (user) {
let profile = null;
try {
const data = await apix({
port: "recruitment",
value: "data.data",
path: "/api/user-profiles/user",
});
profile = data;
delete profile["user"];
user = {
...user,
profile,
};
} catch (ex) {}
const w = window as any;
w.user = JSON.parse(user);
return true;
}
} catch (ex: any) {
return false;
}
}
} else {
try {

10
svg/Down.tsx Normal file
View File

@ -0,0 +1,10 @@
import { SVGProps } from "react";
const SvgComponent = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 7" {...props}>
<path
fill="currentColor"
d="M8 6.5a.47.47 0 0 1-.35-.15l-4.5-4.5c-.2-.2-.2-.51 0-.71s.51-.2.71 0l4.15 4.15 4.14-4.14c.2-.2.51-.2.71 0s.2.51 0 .71l-4.5 4.5c-.1.1-.23.15-.35.15Z"
/>
</svg>
);
export { SvgComponent as Down };

10
svg/Up.tsx Normal file
View File

@ -0,0 +1,10 @@
import { SVGProps } from "react";
const SvgComponent = (props: SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 7" {...props}>
<path
fill="currentColor"
d="M12.5 6a.47.47 0 0 1-.35-.15L8 1.71 3.85 5.85c-.2.2-.51.2-.71 0s-.2-.51 0-.71L7.65.65c.2-.2.51-.2.71 0l4.5 4.5c.2.2.2.51 0 .71-.1.1-.23.15-.35.15Z"
/>
</svg>
);
export { SvgComponent as Up };

37
utils/getLabel.ts Normal file
View File

@ -0,0 +1,37 @@
export const statusMpp = [
{ value: "DRAFTED", label: "Draft" },
{ value: "DRAFT", label: "Draft" },
{ value: "IN_PROGRESS", label: "In Progress" },
{ value: "IN PROGRESS", label: "In Progress" },
{ value: "NEED APPROVAL", label: "Need Approval" },
{ value: "APPROVED", label: "Approved" },
{ value: "REJECTED", label: "Rejected" },
{ value: "COMPLETED", label: "Completed" },
{ value: "SUBMITTED", label: "Submitted" },
{ value: "open", label: "Open" },
{ value: "close", label: "Close" },
{ value: "complete", label: "Complete" },
{ value: "not_open", label: "Not Open" },
{ value: "OFF_BUDGET", label: "Off Budget" },
{ value: "ON_BUDGET", label: "On Budget" },
{ value: "APPLIED", label: "Applied" },
{ value: "PENDING", label: "Pending" },
{ value: "ADMINISTRATIVE_SELECTION", label: "Administrative" },
{ value: "TEST", label: "Test" },
{ value: "INTERVIEW", label: "Interview" },
{ value: "FGD", label: "FGD" },
{ value: "SURAT_PENGANTAR_MASUK", label: "Surat Pengantar Masuk" },
{ value: "SURAT_IZIN_ORANG_TUA", label: "Surat Izin Orang Tua" },
{ value: "FINAL_INTERVIEW", label: "Final Interview" },
{ value: "KARYAWAN_TETAP", label: "Karyawan Tetap" },
{ value: "OFFERING_LETTER", label: "Offering Letter" },
{ value: "CONTRACT_DOCUMENT", label: "Contract Document" },
{ value: "DOCUMENT_CHECKING", label: "Document Checking" },
{ value: "FINAL_RESULT", label: "Final Result" },
];
export const getLabel = (value: string) => {
const status = statusMpp.find(
(item) => item.value.toLowerCase() === value.toLowerCase()
);
return status ? status.label : value;
};