This commit is contained in:
rizrmd 2024-03-04 00:53:28 -07:00
parent 4eebe5bb99
commit ab80962b47
14 changed files with 598 additions and 51 deletions

92
comps/custom/Breadcrumb.tsx Executable file
View File

@ -0,0 +1,92 @@
import { useLocal } from "@/utils/use-local";
import { FC, ReactNode, useEffect } from "react";
import { Skeleton } from "../ui/skeleton";
type BreadcrumbProps = {
on_load: () => Promise<any[]>;
props: any;
};
export const Breadcrumb: FC<BreadcrumbProps> = (_arg) => {
const { on_load } = _arg;
const local = useLocal({
list: [] as { label: string; url: string }[],
status: "init" as "init" | "loading" | "ready",
params: {},
});
useEffect(() => {
(async () => {
if (local.status === "init") {
local.status = "loading";
local.render();
local.list = await on_load();
local.status = "ready";
local.render();
}
})();
}, [on_load]);
return (
<div className="c-w-full c-flex c-items-center c-px-4 c-flex-wrap c-py-2 c-border-b">
{local.status !== "ready" ? (
<Skeleton className="c-h-4 c-w-[80%]" />
) : (
<>
{local.list === null ? (
<>
<h1 className="c-font-semibold c-text-xs md:c-text-base">
Dummy
</h1>
</>
) : (
(local.list || []).map((item, index): ReactNode => {
const lastIndex = local.list.length - 1;
return (
<>
{index === lastIndex ? (
<h1 className="c-font-semibold c-text-xs md:c-text-base">
{item?.label}
</h1>
) : (
<h1
className="c-font-normal c-text-xs md:c-text-base hover:c-cursor-pointer"
onClick={() => {
navigate(item?.url);
}}
>
{item?.label}
</h1>
)}
{index !== lastIndex && (
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-chevron-right"
>
<path d="m9 18 6-6-6-6" />
</svg>
</div>
)}
</>
);
})
)}
</>
)}
</div>
);
};

View File

@ -6,7 +6,9 @@ import { Skeleton } from "../ui/skeleton";
export const Card: FC<{
title: { left: ReactNode; right: ReactNode };
desc: (() => Promise<ReactNode>) | ReactNode;
desc:
| ((arg: { setOnClick: (fn: any) => void }) => Promise<ReactNode>)
| ReactNode;
value: (() => Promise<ReactNode>) | ReactNode;
}> = ({ title, desc, value }) => {
const local = useLocal({
@ -14,6 +16,7 @@ export const Card: FC<{
desc: "" as any,
status_value: "init" as "init" | "loading" | "ready",
status_desc: "init" as "init" | "loading" | "ready",
onClick: null as null | (() => void),
});
useEffect(() => {
@ -38,7 +41,12 @@ export const Card: FC<{
if (!!desc && typeof desc === "function") {
local.status_desc = "loading";
local.render();
const result = desc();
const result = desc({
setOnClick: (fn) => {
local.onClick = fn;
local.render();
},
});
if (typeof result === "object" && result instanceof Promise) {
result.then((val) => {
local.desc = val;
@ -71,7 +79,14 @@ export const Card: FC<{
}
return (
<card.Card className="c-flex c-flex-1 c-items-center">
<card.Card
className="c-flex c-flex-1 c-items-center"
onClick={() => {
if (local.onClick) {
local.onClick();
}
}}
>
<div className={cn("c-p-3 c-text-[14px] c-flex-1")}>
{!!title && (title.left || title.right) && (
<div className="c-tracking-tight c-text-sm c-font-medium c-flex c-justify-between c-space-x-1 mb-1 c-items-center">

View File

@ -6,7 +6,10 @@ import { Skeleton } from "../ui/skeleton";
export const Detail: FC<{
detail: (item: any) => Record<string, [string, string, string]>;
on_load: (arg: { params: any }) => Promise<any>;
on_load: (arg: {
params: any;
bind: (fn: (on_load: any) => void) => void;
}) => Promise<any>;
mode: "standard" | "compact" | "inline";
}> = ({ detail, mode, on_load }) => {
const local = useLocal({
@ -15,6 +18,7 @@ export const Detail: FC<{
pathname: "",
mode: mode,
on_load,
bound: false,
});
if (!isEditor) {
@ -39,7 +43,24 @@ export const Detail: FC<{
}
local.render();
const res = on_load({ params: {} });
const res = on_load({
params: {},
bind: (fn) => {
if (!local.bound) {
local.bound = true;
local.render();
fn(async () => {
local.status = "loading";
local.render();
const item = await on_load({} as any);
local.detail = detail(item);
local.status = "ready";
local.render();
});
}
},
});
if (typeof res === "object" && res instanceof Promise) {
res.then((item) => {
local.detail = detail(item);
@ -70,7 +91,34 @@ export const Detail: FC<{
"c-flex c-relative items-stretch",
mode === "inline"
? "c-flex-row c-my-2"
: "c-flex-col c-flex-1 c-w-full c-h-full "
: "c-flex-col c-flex-1 c-w-full c-h-full ",
isDesktop &&
entries.length > 3 &&
mode === "compact" &&
css`
flex-direction: row !important;
flex-wrap: wrap !important;
border: 1px solid #ddd;
border-radius: 10px;
margin-top: 10px;
margin-bottom: 10px;
&::before {
content: " ";
position: absolute;
left: 49%;
bottom: 0px;
top: 0px;
border-right: 1px solid #ddd;
}
> div {
border-top: 0px;
padding-right: 10px;
padding-left: 10px;
width: 49% !important;
}
`
)}
>
{entries.map(([name, data], idx) => {
@ -90,7 +138,7 @@ export const Detail: FC<{
if (mode === "standard") {
return (
<div key={idx} className="c-flex c-flex-col c-items-stretch">
<div key={idx} className="c-flex c-flex-col c-items-stretch c-pt-3">
<div className="c-flex c-font-bold">{label}</div>
<div className="c-flex">
<Linkable

166
comps/custom/Header.tsx Executable file
View File

@ -0,0 +1,166 @@
import { useLocal } from "@/utils/use-local";
import { FC, ReactNode, useEffect } from "react";
import { Skeleton } from "../ui/skeleton";
export const Header: FC<{
text: string;
back_url: string | (() => void);
props: any;
actions: () => Promise<[string, string | (() => void), ReactNode][]>;
action_mode: "auto" | "buttons" | "sub-header" | "dropdown";
edit_url: string;
}> = ({ text, back_url, props, edit_url, actions, action_mode }) => {
const local = useLocal({
loading: false,
actions: [] as [string, string | (() => void), ReactNode][],
});
useEffect(() => {
(async () => {
if (typeof actions === "function") {
local.loading = true;
local.render();
const res_actions = await actions();
if (Array.isArray(res_actions)) {
local.actions = res_actions;
preload(
res_actions
.filter((e) => typeof e[1] === "string")
.map((e) => e[1]) as string[]
);
}
local.loading = false;
local.render();
}
})();
}, [actions, action_mode]);
const back = () => {
if (isEditor) return;
if (typeof back_url === "function") {
const url = back_url();
if (typeof url === "string") {
if (url === "back") {
history.back();
return;
}
navigate(url);
}
} else {
if (back_url === "back") {
history.back();
return;
}
navigate(back_url);
}
};
return (
<div
{...props}
className={cx(
props.className,
"whitespace-pre-wrap",
css`
min-height: 50px !important;
`
)}
>
{typeof back_url === "string" && preload([back_url])}
{back_url && (
<div className="mr-2" onClick={back}>
<svg
width="25"
height="25"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.84182 3.13514C9.04327 3.32401 9.05348 3.64042 8.86462 3.84188L5.43521 7.49991L8.86462 11.1579C9.05348 11.3594 9.04327 11.6758 8.84182 11.8647C8.64036 12.0535 8.32394 12.0433 8.13508 11.8419L4.38508 7.84188C4.20477 7.64955 4.20477 7.35027 4.38508 7.15794L8.13508 3.15794C8.32394 2.95648 8.64036 2.94628 8.84182 3.13514Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
></path>
</svg>
</div>
)}
<div className="w-full flex flex-row justify-between items-center">
<div className="c-whitespace-nowrap" onClick={back}>
{text}
</div>
<div className="c-flex">
{local.loading && (
<Skeleton
className={cx(
css`
height: 10px;
min-width: 40px;
`
)}
/>
)}
{!local.loading && (
<>
{local.actions.map((e) => {
const [label, to, icon] = e;
return (
<div
className="c-bg-primary hover:c-bg-primary/90 c-p-1 c-rounded-md c-text-sm c-flex c-text-white c-items-center c-space-x-1 c-px-2 c-ml-2"
onClick={() => {
if (isEditor) return;
if (typeof to === "string") {
navigate(to);
} else if (typeof to === "function") {
to();
}
}}
>
<div
className={css`
svg {
max-width: 15px;
}
`}
>
{icon}
</div>
<div>{label}</div>
</div>
);
})}
</>
)}
{/* {edit_url && edit_url !== "" && params.id !== "_" && (
<div
className="c-bg-primary hover:c-bg-primary/90 c-p-1 c-rounded-md c-ml-2"
onClick={() => {
if (isEditor) return;
if (typeof edit_url === "string") {
navigate(edit_url);
}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="white"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-pencil-line"
>
<path d="M12 20h9" />
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
<path d="m15 5 3 3" />
</svg>
</div>
)} */}
</div>
</div>
</div>
);
};

View File

@ -1,4 +1,4 @@
import { FC } from "react";
import { FC, useEffect } from "react";
import { Tabs, TabsList, TabsTrigger } from "../ui/tabs";
import { useLocal } from "@/utils/use-local";
@ -64,6 +64,17 @@ export const Tab: FC<{
}
}
if (!isEditor) {
useEffect(() => {
if (local.mode === "hash") {
local.activeIndex = location.hash.substring(1);
if (!parseInt(local.activeIndex)) {
local.activeIndex = "0";
}
local.render();
}
}, [location.hash]);
}
return (
<div className="c-flex c-flex-1 c-w-full c-flex-col c-items-stretch">
<Tabs

76
comps/custom/Table.tsx Executable file
View File

@ -0,0 +1,76 @@
import { useLocal } from "@/utils/use-local";
import { FC, useEffect } from "react";
import { icon } from "../icon";
type TableProps = {
map_val: Array<{ name: string }>;
};
export const Table: FC<TableProps> = (_args) => {
const { map_val } = _args;
const local = useLocal({
list: [
{
name: "test1",
},
{
name: "test2",
},
] as { name: string }[],
status: "init" as "init" | "loading" | "ready",
});
// useEffect(() => {
// (async () => {
// if (local.status === "init") {
// local.status = "loading";
// local.render();
// local.list = await map_val;
// local.render();
// local.status = "ready";
// local.render();
// }
// })();
// }, [map_val]);
console.log(local.list, "tes");
return (
<div className="c-overflow-x-auto c-w-full">
<table className="c-table-auto c-w-full c-border-collapse c-rounded-lg c-border c-border-gray-300">
<thead>
<tr>
<th className="c-px-4 c-py-2 c-text-center">Nomor</th>
<th className="c-px-4 c-py-2 c-text-center">Header 1</th>
<th className="c-px-4 c-py-2 c-text-center">Action</th>
</tr>
</thead>
<tbody>
{!!local.list &&
local.list.map((item, index) => (
<tr
key={index}
className={index % 2 === 0 ? "c-bg-gray-100" : ""}
>
<td className="c-border c-px-4 c-py-2 c-text-center">
{index + 1}
</td>
<td className="c-border c-px-4 c-py-2">{item.name}</td>
<td className="c-border c-px-4 c-py-2 c-flex c-flex-col c-justify-center c-items-center c-space-y-2">
<button className="c-w-[50px] c-rounded c-flex c-justify-center c-bg-blue-300">
<span className="c-p-2">{icon.update}</span>
</button>
<button className="c-w-[50px] c-rounded c-flex c-justify-center c-bg-red-300">
<span className="c-p-2">{icon.delete}</span>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};

View File

@ -6,31 +6,25 @@ import {
FormLabel,
FormMessage,
} from "@/comps/ui/form";
import { useLocal } from "@/utils/use-local";
import autosize from "autosize";
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 { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import autosize from "autosize";
import { InputMoney } from "./InputMoney";
import { ButtonOptions } from "./ButtonOptions";
import { Date } from "./Date";
import { Datetime } from "./Datetime";
import { Slider } from "@radix-ui/react-slider";
import { InputMoney } from "./InputMoney";
import { PopUpDropdown } from "./PopUpDropdown";
import { SliderOptions } from "./Slider/types";
import { cn } from "@/utils";
export const Field: FC<{
name: string;
label: string;
desc?: string;
form: { hook: UseFormReturn<any, any, undefined>; render: () => void };
form?: { hook: UseFormReturn<any, any, undefined>; render: () => void };
type:
| "text"
| "textarea"
@ -46,7 +40,7 @@ export const Field: FC<{
options: () => Promise<{ value: string; label: string }[]>;
slider_options: () => Promise<SliderOptions>;
}> = ({ name, form, desc, label, type, required, options, slider_options }) => {
const value = form.hook.getValues()[name];
const value = form?.hook.getValues()[name];
const local = useLocal({
dropdown: {
popup: false,
@ -104,14 +98,14 @@ export const Field: FC<{
local.render();
}}
on_select={(value: any) => {
form.hook.setValue(name, value);
form?.hook.setValue(name, value);
}}
title={label}
options={options}
/>
)}
<FormField
control={form.hook.control}
control={form?.hook.control || {} as any}
name={name}
render={({ field }) => (
<FormItem className="c-flex c-flex-1 c-flex-col">
@ -138,7 +132,7 @@ export const Field: FC<{
value,
local.slider.opt
);
form.hook.setValue(name, value);
form?.hook.setValue(name, value);
local.render();
}}
value={local.slider.value}
@ -180,7 +174,7 @@ export const Field: FC<{
{type === "date" && (
<Date
on_select={(value: any) => {
form.hook.setValue(name, value);
form?.hook.setValue(name, value);
}}
/>
)}
@ -188,7 +182,7 @@ export const Field: FC<{
{type === "datetime" && (
<Datetime
on_select={(value: any) => {
form.hook.setValue(name, value);
form?.hook.setValue(name, value);
}}
/>
)}
@ -198,7 +192,7 @@ export const Field: FC<{
options={options}
value={field.value}
on_select={(value: any) => {
form.hook.setValue(name, value);
form?.hook.setValue(name, value);
}}
/>
)}
@ -207,7 +201,7 @@ export const Field: FC<{
<InputMoney
value={field.value}
on_select={(value: any) => {
form.hook.setValue(name, value);
form?.hook.setValue(name, value);
}}
/>
)}

View File

@ -1,4 +1,5 @@
import { Form as FForm } from "@/comps/ui/form";
import { useLocal } from "@/utils/use-local";
import { FC } from "react";
import { useForm } from "react-hook-form";
@ -8,13 +9,22 @@ export const Form: FC<{
body: any;
form: { hook: any; render: () => void };
PassProp: any;
}> = ({ on_load, body, form, PassProp, on_submit }) => {
layout: "auto" | "1-col" | "2-col";
}> = ({ on_load, body, form, PassProp, on_submit, layout: _layout }) => {
const form_hook = useForm<any>({
defaultValues: on_load,
});
const local = useLocal({
el: null as any,
layout: "unknown" as "unknown" | "2-col" | "1-col",
});
form.hook = form_hook;
let layout = _layout || "auto";
if (layout !== "auto") local.layout = layout;
return (
<FForm {...form_hook}>
<form
@ -27,7 +37,44 @@ export const Form: FC<{
on_submit({ form: form.hook.getValues(), error: {} });
}}
>
<div className="absolute inset-0">
<div
ref={(el) => {
if (el && layout === "auto" && local.layout === "unknown") {
let cur: any = el;
let i = 0;
while (
cur.parentNode &&
cur.getBoundingClientRect().width === 0
) {
cur = cur.parentNode;
i++;
if (i > 10) {
break;
}
}
if (cur.getBoundingClientRect().width < 500) {
local.layout = "1-col";
} else {
local.layout = "2-col";
}
local.render();
}
}}
className={cx(
local.layout === "unknown" && "c-hidden",
local.layout === "2-col" &&
css`
> div {
flex-direction: row;
flex-wrap: wrap;
> div {
width: 50%;
}
}
`
)}
>
<PassProp
submit={() => {
on_submit({ form: form.hook.getValues(), error: {} });

View File

@ -1,4 +1,48 @@
import { Bell, Calendar, ChevronRight, UsersRound, ChevronDown, Contact2, Drill, HelpCircle, Home, LayoutList, ListChecks, ListTodo, PenBoxIcon, QrCode, UserCog, Workflow, Settings, Newspaper, PanelRightOpen, PanelLeftOpen, LayoutDashboard, Building, Box, Package, Blocks, Megaphone, MousePointerSquare, Siren, Ship, HeartHandshake, StickyNote, Volume2, Wallet, FileDown, Menu, X, LogOut, Rocket, BookOpen } from "lucide-react";
import {
Bell,
Calendar,
ChevronRight,
UsersRound,
ChevronDown,
Contact2,
Drill,
HelpCircle,
Home,
LayoutList,
ListChecks,
ListTodo,
PenBoxIcon,
QrCode,
UserCog,
Workflow,
Settings,
Newspaper,
PanelRightOpen,
PanelLeftOpen,
LayoutDashboard,
Building,
Box,
Package,
Blocks,
Megaphone,
MousePointerSquare,
Siren,
Ship,
HeartHandshake,
StickyNote,
Volume2,
Wallet,
FileDown,
Menu,
X,
LogOut,
Rocket,
BookOpen,
Pencil,
Plus,
Delete,
Edit,
} from "lucide-react";
export const icon = {
home: <Home />,
@ -14,6 +58,8 @@ export const icon = {
manage_user: <UserCog />,
notification: <Bell />,
profile: <Contact2 />,
pencil: <Pencil />,
plus: <Plus />,
bell: <Bell />,
bellSmall: <Bell className={`c-w-4`} fill="currentColor" />,
bellSmallTransparent: <Bell className={`c-w-4`} />,
@ -53,6 +99,20 @@ export const icon = {
releaseSmallTransparent: <Rocket className={`c-w-4`} />,
newsSmall: <BookOpen className={`c-w-4`} fill="currentColor" />,
newsSmallTransparent: <BookOpen className={`c-w-4`} />,
scan: (
<svg
width="28"
height="28"
viewBox="0 0 28 28"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M27.5 20V27.5H0.5V20H3.5V24.5H24.5V20H27.5ZM0.5 12.5H27.5V15.5H0.5V12.5ZM27.5 8H24.5V3.5H3.5V8H0.5V0.5H27.5V8Z"
fill="white"
/>
</svg>
),
delete: <Delete />,
update: <Edit />
};

View File

@ -24,7 +24,7 @@ export const List: FC<ListProp> = (_arg) => {
list: [] as any[],
});
if (isEditor) {
if (isEditor || typeof on_load !== 'function') {
return <ListDummy {..._arg} />;
}
@ -35,6 +35,7 @@ export const List: FC<ListProp> = (_arg) => {
useEffect(() => {
(async () => {
if (typeof on_load === "function") {
if (local.status === "init") {
local.status = "loading";
local.render();
@ -44,6 +45,7 @@ export const List: FC<ListProp> = (_arg) => {
local.status = "ready";
local.render();
}
}
})();
}, [on_load]);
@ -61,13 +63,16 @@ export const List: FC<ListProp> = (_arg) => {
<ListDummy {..._arg} />
) : (
(local.list || []).map((item, idx) => {
const mapped = map_val(item);
const val = (...arg: any[]) => {
const value = get(map_val(item), `${arg.join("")}`);
const value = get(mapped, `${arg.join("")}`);
return value;
};
return (
<div key={item} className="c-border-b">
<PassProp item={val}>{row}</PassProp>
<PassProp item={val} row={mapped}>
{row}
</PassProp>
</div>
);
})

View File

@ -56,4 +56,21 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
);
Button.displayName = "Button";
export { Button, buttonVariants };
const FloatButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant, className, ...props }, ref) => {
return (
<button
className={cn(
buttonVariants({ variant, className }),
`btn-${
variant || "default"
} btn c-transition-all c-duration-300 c-w-10 c-h-10 c-rounded-full c-z-50 c-absolute c-bottom-7 c-right-6 c-shadow-sm`
)}
ref={ref}
{...props}
/>
);
}
);
export { Button, buttonVariants, FloatButton };

View File

@ -7,6 +7,9 @@ export { formatMoney } from "@/comps/form/InputMoney";
export { icon } from "@/comps/icon";
export { List } from "@/comps/list/List";
export { Slider } from "@/comps/ui/slider";
export { longDate, shortDate } from "@/utils/date";
export { Button } from "@/comps/ui/button";
export * from "@/utils/date";
export { Button, FloatButton } from "@/comps/ui/button";
export { getPathname } from "@/utils/pathname";
export { Breadcrumb } from "./comps/custom/Breadcrumb";
export { Header } from "./comps/custom/Header";
export { Table } from "./comps/custom/Table";

View File

@ -1,4 +1,4 @@
import { format } from "date-fns";
import { format, formatDistanceToNow } from "date-fns";
export const longDate = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
@ -13,3 +13,10 @@ export const shortDate = (date: string | Date) => {
}
return "-";
};
export const timeAgo = (date: string | Date) => {
if (date instanceof Date || typeof date === "string") {
return formatDistanceToNow(date, { addSuffix: true });
}
return "-";
};

View File

@ -5,8 +5,14 @@ export const getPathname = () => {
location.pathname.startsWith("/prod") ||
location.pathname.startsWith("/deploy")
) {
const hash = location.hash;
if (hash !== "") {
return "/" + location.pathname.split("/").slice(3).join("/") + hash;
} else {
return "/" + location.pathname.split("/").slice(3).join("/");
}
}
}
return location.pathname;
};