commit 25976636fa91530b1a32516ef6dea0b730b51f19 Author: Rizky Date: Mon Feb 5 20:11:29 2024 +0700 add diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/lib/comps/custom/Card.tsx b/lib/comps/custom/Card.tsx new file mode 100755 index 0000000..8fb15b6 --- /dev/null +++ b/lib/comps/custom/Card.tsx @@ -0,0 +1,114 @@ +import { FC, ReactNode, isValidElement, useEffect } from "react"; +import * as card from "../ui/card"; +import { cn } from "@/utils"; +import { useLocal } from "@/utils/use-local"; +import { Skeleton } from "../ui/skeleton"; + +export const Card: FC<{ + title: { left: ReactNode; right: ReactNode }; + desc: (() => Promise) | ReactNode; + value: (() => Promise) | ReactNode; +}> = ({ title, desc, value }) => { + const local = useLocal({ + value: "" as any, + desc: "" as any, + status_value: "init" as "init" | "loading" | "ready", + status_desc: "init" as "init" | "loading" | "ready", + }); + + useEffect(() => { + if (!isEditor) { + if (!!value && typeof value === "function") { + local.status_value = "loading"; + local.render(); + const result = value(); + if (typeof result === "object" && result instanceof Promise) { + result.then((val) => { + local.value = val; + local.status_value = "ready"; + local.render(); + }); + } else { + local.value = result; + local.status_value = "ready"; + local.render(); + } + } + + if (!!desc && typeof desc === "function") { + local.status_desc = "loading"; + local.render(); + const result = desc(); + if (typeof result === "object" && result instanceof Promise) { + result.then((val) => { + local.desc = val; + local.status_desc = "ready"; + local.render(); + }); + } else { + local.desc = result; + local.status_desc = "ready"; + local.render(); + } + } + } + }, []); + + if (typeof desc !== "function") { + local.status_desc = "ready"; + local.desc = desc; + } else if (isEditor) { + local.desc = typeof desc === "function" ? "..." : desc; + local.status_desc = "ready"; + } + + if (typeof value !== "function") { + local.status_value = "ready"; + local.value = value; + } else if (isEditor) { + local.value = "..."; + local.status_value = "ready"; + } + + return ( + +
+ {!!title && (title.left || title.right) && ( +
+
{title.left}
+ {title.right && ( +
{title.right}
+ )} +
+ )} + + {local.status_value === "ready" ? ( + formatObject(local.value) + ) : ( +
+ + +
+ )} +
+ + {local.status_desc === "ready" ? ( + formatObject(local.desc) + ) : ( +
+ +
+ )} +
+
+
+ ); +}; + +const formatObject = (val: any) => { + if (typeof val === "object") { + if (isValidElement(val)) return val; + else return JSON.stringify(val); + } + return val; +}; diff --git a/lib/comps/custom/Detail.tsx b/lib/comps/custom/Detail.tsx new file mode 100755 index 0000000..d2286d6 --- /dev/null +++ b/lib/comps/custom/Detail.tsx @@ -0,0 +1,191 @@ +import { useLocal } from "@/utils/use-local"; +import { cx } from "class-variance-authority"; +import { FC, useEffect } from "react"; +import { Skeleton } from "../ui/skeleton"; + +export const Detail: FC<{ + detail: (item: any) => Record; + on_load: (arg: { params: any }) => Promise; + mode: "standard" | "compact" | "inline"; +}> = ({ detail, mode, on_load }) => { + const local = useLocal({ + status: "init" as "init" | "loading" | "ready", + detail: null as any, + }); + + if (!isEditor) { + useEffect(() => { + if (local.status === "init" && typeof on_load === "function") { + local.status = "loading"; + local.detail = detail({}); + local.render(); + const res = on_load({ params: {} }); + if (typeof res === "object" && res instanceof Promise) { + res.then((item) => { + local.detail = detail(item); + local.status = "ready"; + local.render(); + }); + } else { + local.detail = detail(res); + local.status = "ready"; + local.render(); + } + } + }, [on_load]); + } + let values = {}; + + if (!isEditor && typeof on_load === "function") { + values = local.detail || {}; + } else { + values = detail(null); + local.status = "ready"; + } + + const entries = Object.entries(values); + return ( +
+ {entries.map(([name, data], idx) => { + const is_first = idx === 0; + const is_last = idx === entries.length - 1; + if ( + typeof data !== "object" || + !data || + (typeof data === "object" && !Array.isArray(data)) + ) + return null; + const [label, sample, link] = data; + + if (link) { + preload(link); + } + + if (mode === "standard") { + return ( +
+
{label}
+
+ +
+
+ ); + } else if (mode === "compact") { + return ( +
+
+ {label} +
+
+ +
+
+ ); + } else { + return ( +
+
{label}
+
+ +
+
+ ); + } + })} +
+ ); +}; + +const Linkable: FC<{ + sample?: string; + link?: string; + status: "init" | "loading" | "ready"; +}> = ({ sample, link, status }) => { + const loading = ( + + ); + + if (!link) { + if (status !== "ready") return loading; + return sample || "-"; + } + + return ( +
{ + if (!isEditor) { + navigate(link); + } + }} + > + {status === "ready" ? ( + <> +
+ {sample || '-'} +
+ + ) : ( +
+ {loading} +
+ )} + + + +
+ ); +}; diff --git a/lib/comps/custom/Tab.tsx b/lib/comps/custom/Tab.tsx new file mode 100755 index 0000000..af602db --- /dev/null +++ b/lib/comps/custom/Tab.tsx @@ -0,0 +1,116 @@ +import { FC } from "react"; +import { Tabs, TabsList, TabsTrigger } from "../ui/tabs"; +import { useLocal } from "@/utils/use-local"; + +export const Tab: FC<{ + tabs: (arg: { count: (string | number)[] }) => { + label: string; + navigate: string; + count: string; + color?: string; + onClick?: () => Promise | void; + }[]; + active: string; + body: any; + on_load?: () => any; + PassProp: any; +}> = ({ tabs, active, body, PassProp, on_load }) => { + const local = useLocal({ + active, + count: [] as (number | string)[], + status: "init" as "init" | "load" | "ready", + }, () => { + if (local.status === "init") { + if (typeof on_load === "function" && !isEditor) { + local.status = "load"; + const res = on_load(); + if (typeof res === "object" && res instanceof Promise) { + res.then((value) => { + local.count = value; + local.status = "ready"; + local.render(); + }); + } else { + local.count = res; + local.status = "ready"; + } + } else { + local.status = "ready"; + } + } + }); + + const all_tabs = tabs({ count: local.count || [] }); + return ( +
+ + + {all_tabs.map((e, idx) => { + if (e.navigate) { + preload(e.navigate); + } + return ( + { + local.active = idx.toString(); + local.render(); + if (e.navigate) { + navigate(e.navigate); + } + }} + className={cx( + css` + padding: 0px !important; + margin: 0px 0px 0px ${idx === 0 ? 0 : 5}px; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + ` + )} + > +
+
{e.label}
+ {e.count && ( +
+ {local.status != "ready" ? "..." : e.count} +
+ )} +
+
+ ); + })} +
+
+
+ {body} +
+
+ ); +}; diff --git a/lib/comps/form/ButtonOptions/index.tsx b/lib/comps/form/ButtonOptions/index.tsx new file mode 100755 index 0000000..8467226 --- /dev/null +++ b/lib/comps/form/ButtonOptions/index.tsx @@ -0,0 +1,46 @@ +import { useLocal } from "@/utils/use-local"; +import { FC, useEffect } from "react"; +import { Button } from "../../ui/button"; + +export const ButtonOptions: FC<{ + on_select: (val: any) => void; + options: () => Promise<{ value: string; label: string }[]>; + value: string +}> = ({ options, on_select, value }) => { + const local = useLocal({ + list: [] as { value: string; label: string }[], + status: "init" as "init" | "loading" | "ready", + }); + + useEffect(() => { + if (local.status === "init") { + local.status = "loading"; + local.render(); + options().then((result) => { + local.list = result; + + local.status = "ready"; + local.render(); + }); + } + }, [options]); + + return ( +
+ {!!local.list && + local.list.map((item, index) => ( + + ))} +
+ ); +}; diff --git a/lib/comps/form/Date/index.tsx b/lib/comps/form/Date/index.tsx new file mode 100755 index 0000000..687457c --- /dev/null +++ b/lib/comps/form/Date/index.tsx @@ -0,0 +1,16 @@ +import { FC } from "react"; + +export const Date: FC<{ + on_select: (val: any) => void; +}> = ({ on_select }) => { + return ( + { + on_select(event.target.value); + }} + > + ); +}; diff --git a/lib/comps/form/Datetime/index.tsx b/lib/comps/form/Datetime/index.tsx new file mode 100755 index 0000000..95596e4 --- /dev/null +++ b/lib/comps/form/Datetime/index.tsx @@ -0,0 +1,16 @@ +import { FC } from "react"; + +export const Datetime: FC<{ + on_select: (val: any) => void; +}> = ({ on_select }) => { + return ( + { + on_select(event.target.value); + }} + /> + ); +}; diff --git a/lib/comps/form/Field.tsx b/lib/comps/form/Field.tsx new file mode 100755 index 0000000..71a186a --- /dev/null +++ b/lib/comps/form/Field.tsx @@ -0,0 +1,241 @@ +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/comps/ui/form"; +import { FC, useEffect, useRef } from "react"; +import { UseFormReturn } from "react-hook-form"; +import { Input } from "../ui/input"; +import { useLocal } from "@/utils/use-local"; +import { Button } from "../ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@/comps/ui/popover"; +import { format } from "date-fns"; +import { Calendar } from "@/comps/ui/calendar"; +import { Calendar as CalendarIcon } from "lucide-react"; +import { PopUpDropdown } from "./PopUpDropdown"; +import { ButtonOptions } from "./ButtonOptions"; +import { Textarea } from "../ui/textarea"; +import autosize from "autosize"; +import { InputMoney } from "./InputMoney"; +import { Date } from "./Date"; +import { Datetime } from "./Datetime"; +import { Slider } from "@radix-ui/react-slider"; +import { SliderOptions } from "./Slider/types"; +import { cn } from "@/utils"; + +export const Field: FC<{ + name: string; + label: string; + desc?: string; + form: { hook: UseFormReturn; render: () => void }; + type: + | "text" + | "textarea" + | "dropdown" + | "password" + | "button-options" + | "date" + | "datetime" + | "money" + | "slider" + | "master-linkF"; + required: "y" | "n"; + options: () => Promise<{ value: string; label: string }[]>; + slider_options: () => Promise; +}> = ({ name, form, desc, label, type, required, options, slider_options }) => { + const value = form.hook.getValues()[name]; + const local = useLocal({ + dropdown: { + popup: false, + }, + date: { + // label: "", + popup: false, + }, + slider: { + value: 0, + opt: { + step: 1, + min: { value: 0, label: "Start" }, + max: { value: 100, label: "End" }, + } as SliderOptions, + status: "init" as "init" | "loading" | "ready", + }, + }); + + const textAreaRef = useRef(); + useEffect(() => { + autosize(textAreaRef.current); + return () => { + autosize.destroy(textAreaRef.current); + }; + }, []); + + useEffect(() => { + if (type === "slider") { + local.slider.value = parseSliderValue(value, local.slider.opt); + if (typeof slider_options === "function") { + if (local.slider.status === "init") { + local.slider.status = "ready"; + local.render(); + (async () => { + const res = await slider_options(); + local.slider.opt = res; + local.render(); + })(); + } + } else { + local.slider.status = "ready"; + local.render(); + } + } + }, [value]); + + return ( + <> + {local.dropdown.popup && ( + { + local.dropdown.popup = false; + local.render(); + }} + on_select={(value: any) => { + form.hook.setValue(name, value); + }} + title={label} + options={options} + /> + )} + ( + + + {label} + {required === "y" &&

*

} +
+ + <> + {type === "slider" && ( +
+
+
{local.slider.opt.min.value}
+
{local.slider.opt.min.label}
+
+
+ { + const value = e.currentTarget.value; + + local.slider.value = parseSliderValue( + value, + local.slider.opt + ); + form.hook.setValue(name, value); + local.render(); + }} + value={local.slider.value} + min={local.slider.opt.min.value} + max={local.slider.opt.max.value} + /> +
+ {local.slider.value} +
+
+ +
+
{local.slider.opt.max.value}
+
{local.slider.opt.max.label}
+
+
+ )} + + {["text", "password"].includes(type) && ( + + )} + + {type === "textarea" && ( +