Updates
This commit is contained in:
parent
9505331165
commit
69d432b6af
1
.gitignore
vendored
1
.gitignore
vendored
@ -38,3 +38,4 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
dsql-schema-to-typedef.json
|
||||
@ -40,7 +40,7 @@ export default function ModalComponent({ open, setOpen, ...props }: Props) {
|
||||
<Paper
|
||||
{..._.omit(props, ["targetWrapperProps"])}
|
||||
className={twMerge(
|
||||
"z-10 max-w-[500px] bg-background-light dark:bg-background-dark",
|
||||
"z-10 max-w-modal bg-background-light dark:bg-background-dark",
|
||||
"w-full relative max-h-[95vh] overflow-y-auto",
|
||||
"twui-modal-content",
|
||||
props.className
|
||||
|
||||
@ -62,6 +62,7 @@
|
||||
--radius-default-xl: 10px;
|
||||
|
||||
--container-container: 1200px;
|
||||
--container-modal: 800px;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
@ -53,8 +53,6 @@ export default function TWUIDocsRightAside({
|
||||
|
||||
const nextElementH3 = nextElement.querySelector("h3");
|
||||
|
||||
console.log("nextElement", nextElement);
|
||||
|
||||
const isNextElementH2 =
|
||||
nextElement.querySelector("h2") !== null;
|
||||
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import React, { MutableRefObject } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import AceEditorModes from "./ace-editor-modes";
|
||||
import { AceEditorOptions } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
|
||||
export type AceEditorComponentType = {
|
||||
editorRef?: MutableRefObject<AceAjax.Editor>;
|
||||
editorRef?: MutableRefObject<AceAjax.Editor | undefined>;
|
||||
readOnly?: boolean;
|
||||
/** Function to call when Ctrl+Enter is pressed */
|
||||
ctrlEnterFn?: (editor: AceAjax.Editor) => void;
|
||||
content?: string;
|
||||
placeholder?: string;
|
||||
title?: string;
|
||||
mode?: (typeof AceEditorModes)[number];
|
||||
fontSize?: string;
|
||||
previewMode?: boolean;
|
||||
@ -18,7 +20,9 @@ export type AceEditorComponentType = {
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
refresh?: number;
|
||||
refreshDepArr?: any[];
|
||||
editorOptions?: AceEditorOptions;
|
||||
showLabel?: boolean;
|
||||
};
|
||||
|
||||
let timeout: any;
|
||||
@ -40,13 +44,15 @@ export default function AceEditor({
|
||||
previewMode,
|
||||
onChange,
|
||||
delay = 500,
|
||||
refresh: externalRefresh,
|
||||
refreshDepArr,
|
||||
wrapperProps,
|
||||
editorOptions,
|
||||
showLabel,
|
||||
title,
|
||||
}: AceEditorComponentType) {
|
||||
try {
|
||||
const editorElementRef = React.useRef<HTMLDivElement>(null);
|
||||
// const editorRefInstance = React.useRef<AceAjax.Editor>(null);
|
||||
const editorRefInstance = React.useRef<any>(null);
|
||||
const editorElementRef = React.useRef<HTMLDivElement>(undefined);
|
||||
const editorRefInstance = React.useRef<AceAjax.Editor>(undefined);
|
||||
|
||||
const [refresh, setRefresh] = React.useState(0);
|
||||
const [darkMode, setDarkMode] = React.useState(false);
|
||||
@ -84,9 +90,7 @@ export default function AceEditor({
|
||||
showLineNumbers: previewMode ? false : true,
|
||||
wrap: true,
|
||||
wrapMethod: "code",
|
||||
// onchange: (e) => {
|
||||
// console.log(e);
|
||||
// },
|
||||
...editorOptions,
|
||||
});
|
||||
|
||||
editor.commands.addCommand({
|
||||
@ -103,7 +107,9 @@ export default function AceEditor({
|
||||
clearTimeout(timeout);
|
||||
|
||||
setTimeout(() => {
|
||||
onChange(editor.getValue());
|
||||
try {
|
||||
onChange(editor.getValue());
|
||||
} catch (error) {}
|
||||
}, delay);
|
||||
}
|
||||
});
|
||||
@ -114,7 +120,7 @@ export default function AceEditor({
|
||||
return function () {
|
||||
editor.destroy();
|
||||
};
|
||||
}, [refresh, darkMode, ready, externalRefresh]);
|
||||
}, [refresh, darkMode, ready, mode, ...(refreshDepArr || [])]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const htmlClassName = document.documentElement.className;
|
||||
@ -129,12 +135,23 @@ export default function AceEditor({
|
||||
<div
|
||||
{...wrapperProps}
|
||||
className={twMerge(
|
||||
"w-full h-[400px] block rounded-default overflow-hidden",
|
||||
"border border-slate-200 border-solid",
|
||||
"w-full h-[400px] block rounded-default",
|
||||
"border border-slate-200 border-solid relative",
|
||||
"dark:border-white/20",
|
||||
wrapperProps?.className
|
||||
showLabel && title ? "pt-4" : "",
|
||||
wrapperProps?.className,
|
||||
)}
|
||||
>
|
||||
{showLabel && title ? (
|
||||
<label
|
||||
className={twMerge(
|
||||
"bg-background-light dark:bg-background-dark text-xs",
|
||||
"-top-3 left-2 px-2 py-1 absolute z-10",
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</label>
|
||||
) : null}
|
||||
<div
|
||||
ref={editorElementRef as any}
|
||||
className="w-full h-full"
|
||||
|
||||
@ -13,6 +13,7 @@ import Button from "../layout/Button";
|
||||
|
||||
export type TWUI_LINK_LIST_LINK_OBJECT = {
|
||||
title?: string;
|
||||
component?: ReactNode;
|
||||
url?: string;
|
||||
strict?: boolean;
|
||||
icon?: ReactNode;
|
||||
@ -70,7 +71,7 @@ export default function LinkList({
|
||||
className={twMerge(
|
||||
"flex flex-row items-center gap-1",
|
||||
"twui-link-list",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{links
|
||||
@ -104,7 +105,7 @@ export default function LinkList({
|
||||
{...link.buttonProps}
|
||||
className={twMerge(
|
||||
"p-2 cursor-pointer whitespace-nowrap",
|
||||
linkProps?.className
|
||||
linkProps?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
link.onClick?.(e);
|
||||
@ -113,7 +114,7 @@ export default function LinkList({
|
||||
>
|
||||
<Row>
|
||||
{link.icon}
|
||||
{link.title}
|
||||
{link.component || link.title}
|
||||
</Row>
|
||||
</Button>
|
||||
{finalDivider}
|
||||
@ -131,7 +132,7 @@ export default function LinkList({
|
||||
className={twMerge(
|
||||
"p-2 cursor-pointer whitespace-nowrap",
|
||||
linkProps?.className,
|
||||
link.linkProps?.className
|
||||
link.linkProps?.className,
|
||||
)}
|
||||
strict={link.strict}
|
||||
onClick={(e) => {
|
||||
@ -144,7 +145,7 @@ export default function LinkList({
|
||||
link.iconPosition == "before"
|
||||
? link.icon
|
||||
: null}
|
||||
{link.title}
|
||||
{link.component || link.title}
|
||||
{link.iconPosition == "after"
|
||||
? link.icon
|
||||
: null}
|
||||
|
||||
@ -31,14 +31,18 @@ export default function Loading({ size, svgClassName, ...props }: Props) {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div role="status" {...props}>
|
||||
<div
|
||||
role="status"
|
||||
{...props}
|
||||
className={twMerge(`twui-loading`, props.className)}
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={twMerge(
|
||||
"text-gray animate-spin dark:text-gray-dark fill-primary",
|
||||
"dark:fill-white twui-loading",
|
||||
sizeClassName,
|
||||
svgClassName
|
||||
svgClassName,
|
||||
)}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
|
||||
@ -11,6 +11,7 @@ type Props = DetailedHTMLProps<
|
||||
> & {
|
||||
loadingProps?: ComponentProps<typeof Loading>;
|
||||
label?: string;
|
||||
fixed?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -20,16 +21,18 @@ type Props = DetailedHTMLProps<
|
||||
export default function LoadingOverlay({
|
||||
loadingProps,
|
||||
label,
|
||||
fixed,
|
||||
...props
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"absolute top-0 left-0 w-full h-full z-[500]",
|
||||
"top-0 left-0 w-full h-full z-[500]",
|
||||
"bg-background-light/90 dark:bg-background-dark/90",
|
||||
fixed ? "fixed" : "absolute",
|
||||
props.className,
|
||||
"twui-loading-overlay"
|
||||
"twui-loading-overlay",
|
||||
)}
|
||||
>
|
||||
<Center>
|
||||
|
||||
@ -146,7 +146,11 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
|
||||
{target ? (
|
||||
<div
|
||||
{...targetWrapperProps}
|
||||
onClick={(e) => setOpen(!open)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpen(!open);
|
||||
}}
|
||||
ref={finalTargetRef}
|
||||
onMouseEnter={
|
||||
isPopover && (trigger === "hover" || hoverOpen)
|
||||
|
||||
@ -9,6 +9,7 @@ export const TWUIPrismLanguages = ["shell", "javascript"] as const;
|
||||
|
||||
type Props = {
|
||||
content: string;
|
||||
refresh?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -16,7 +17,7 @@ type Props = {
|
||||
*
|
||||
* @className `twui-remote-code-block-wrapper`
|
||||
*/
|
||||
export default function RemoteCodeBlock({ content }: Props) {
|
||||
export default function RemoteCodeBlock({ content, refresh }: Props) {
|
||||
const [mdxSource, setMdxSource] =
|
||||
React.useState<MDXRemoteSerializeResult<any>>();
|
||||
|
||||
@ -31,7 +32,7 @@ export default function RemoteCodeBlock({ content }: Props) {
|
||||
}).then((mdxSrc) => {
|
||||
setMdxSource(mdxSrc);
|
||||
});
|
||||
}, []);
|
||||
}, [refresh]);
|
||||
|
||||
if (!mdxSource) {
|
||||
return null;
|
||||
|
||||
@ -78,12 +78,12 @@ export default function Search<KeyType extends string>({
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
className={twMerge(
|
||||
"rounded-r-none",
|
||||
"rounded-r-none!",
|
||||
"twui-search-input",
|
||||
inputProps?.className
|
||||
)}
|
||||
wrapperProps={{
|
||||
className: "rounded-r-none",
|
||||
className: "rounded-r-none!",
|
||||
}}
|
||||
componentRef={inputRef}
|
||||
/>
|
||||
@ -93,7 +93,7 @@ export default function Search<KeyType extends string>({
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className={twMerge(
|
||||
"rounded-l-none ml-[1px]",
|
||||
"rounded-l-none! ml-[1px]",
|
||||
"twui-search-button",
|
||||
buttonProps?.className
|
||||
)}
|
||||
|
||||
@ -14,6 +14,7 @@ type StarProps = {
|
||||
starProps?: LucideProps;
|
||||
allowRating?: boolean;
|
||||
setValueExternal?: React.Dispatch<React.SetStateAction<number>>;
|
||||
changeHandler?: (value: number) => void;
|
||||
};
|
||||
|
||||
export type TWUI_STAR_RATING_PROPS = DetailedHTMLProps<
|
||||
@ -35,6 +36,7 @@ export default function StarRating({
|
||||
starProps,
|
||||
allowRating,
|
||||
setValueExternal,
|
||||
changeHandler,
|
||||
...props
|
||||
}: TWUI_STAR_RATING_PROPS) {
|
||||
const totalArray = Array(total).fill(null);
|
||||
@ -58,7 +60,7 @@ export default function StarRating({
|
||||
className={twMerge(
|
||||
"flex flex-row items-center gap-0 -ml-[2px]",
|
||||
"twui-star-rating",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
sectionHovered.current = true;
|
||||
@ -68,6 +70,8 @@ export default function StarRating({
|
||||
}}
|
||||
>
|
||||
{totalArray.map((_, index) => {
|
||||
const isActive = index + 1 <= finalValue;
|
||||
|
||||
return (
|
||||
<StarComponent
|
||||
{...{
|
||||
@ -83,6 +87,8 @@ export default function StarRating({
|
||||
selectedStarValue,
|
||||
sectionHovered,
|
||||
setSelectedStarValue,
|
||||
isActive,
|
||||
changeHandler,
|
||||
}}
|
||||
key={index}
|
||||
/>
|
||||
@ -93,34 +99,31 @@ export default function StarRating({
|
||||
}
|
||||
|
||||
function StarComponent({
|
||||
value = 0,
|
||||
size = 20,
|
||||
starProps,
|
||||
index,
|
||||
allowRating,
|
||||
finalValue,
|
||||
setFinalValue,
|
||||
starClicked,
|
||||
sectionHovered,
|
||||
setSelectedStarValue,
|
||||
selectedStarValue,
|
||||
isActive,
|
||||
changeHandler,
|
||||
}: StarProps & {
|
||||
index: number;
|
||||
finalValue: number;
|
||||
setFinalValue: React.Dispatch<React.SetStateAction<number>>;
|
||||
setSelectedStarValue: React.Dispatch<React.SetStateAction<number>>;
|
||||
starClicked: React.MutableRefObject<boolean>;
|
||||
sectionHovered: React.MutableRefObject<boolean>;
|
||||
selectedStarValue: number;
|
||||
isActive: boolean;
|
||||
}) {
|
||||
const isActive = index < finalValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge("p-[2px]", allowRating && "cursor-pointer")}
|
||||
onMouseEnter={() => {
|
||||
if (!allowRating) return;
|
||||
|
||||
setFinalValue(index + 1);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
@ -145,6 +148,7 @@ function StarComponent({
|
||||
|
||||
starClicked.current = true;
|
||||
setSelectedStarValue(index + 1);
|
||||
changeHandler?.(index + 1);
|
||||
}}
|
||||
>
|
||||
<Star
|
||||
@ -155,7 +159,7 @@ function StarComponent({
|
||||
"text-orange-500 dark:text-orange-400 fill-orange-500 dark:fill-orange-400",
|
||||
// allowRating &&
|
||||
// "hover:text-orange-500 hover:dark:text-orange-400 hover:fill-orange-500 hover:dark:fill-orange-400",
|
||||
starProps?.className
|
||||
starProps?.className,
|
||||
)}
|
||||
{...starProps}
|
||||
/>
|
||||
|
||||
@ -27,6 +27,8 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
|
||||
switchComponent?: ReactNode;
|
||||
setActiveValue?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
changeHandler?: (value: TWUITabsObject) => void;
|
||||
defaultValue?: string | null;
|
||||
hrefUpdate?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -47,6 +49,8 @@ export default function Tabs({
|
||||
switchComponent,
|
||||
setActiveValue: existingSetActiveValue,
|
||||
changeHandler,
|
||||
defaultValue,
|
||||
hrefUpdate,
|
||||
...props
|
||||
}: TWUI_TOGGLE_PROPS) {
|
||||
const finalTabsContentArray = tabsContentArray
|
||||
@ -54,31 +58,60 @@ export default function Tabs({
|
||||
.filter((ct) => Boolean(ct?.title)) as TWUITabsObject[];
|
||||
|
||||
const values = finalTabsContentArray.map(
|
||||
(obj) => obj.value || twuiSlugify(obj.title)
|
||||
(obj) => obj.value || twuiSlugify(obj.title),
|
||||
);
|
||||
|
||||
const defaultActiveObj = finalTabsContentArray.find(
|
||||
(ctn) => ctn.defaultActive
|
||||
(ctn) => ctn.defaultActive,
|
||||
);
|
||||
|
||||
const [activeValue, setActiveValue] = React.useState(
|
||||
defaultActiveObj
|
||||
? defaultActiveObj?.value || twuiSlugify(defaultActiveObj.title)
|
||||
: values[0] || undefined
|
||||
defaultValue
|
||||
? defaultValue
|
||||
: defaultActiveObj
|
||||
? defaultActiveObj?.value || twuiSlugify(defaultActiveObj.title)
|
||||
: values[0] || undefined,
|
||||
);
|
||||
const [ready, setReady] = React.useState(false);
|
||||
|
||||
const targetContent = finalTabsContentArray.find(
|
||||
(ctn) =>
|
||||
ctn.value == activeValue || twuiSlugify(ctn.title) == activeValue
|
||||
ctn.value == activeValue || twuiSlugify(ctn.title) == activeValue,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ready) return;
|
||||
existingSetActiveValue?.(activeValue);
|
||||
if (targetContent && activeValue) {
|
||||
changeHandler?.(targetContent);
|
||||
|
||||
if (hrefUpdate) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("tab", activeValue);
|
||||
window.history.pushState({}, "", url);
|
||||
}
|
||||
}
|
||||
}, [activeValue]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hrefUpdate) {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
const activeTab = url.searchParams.get("tab");
|
||||
|
||||
if (activeTab && activeValue !== activeTab) {
|
||||
setActiveValue(undefined);
|
||||
setActiveValue(activeTab);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setReady(true);
|
||||
}, 500);
|
||||
} else {
|
||||
setReady(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
{...props}
|
||||
@ -89,7 +122,7 @@ export default function Tabs({
|
||||
className={twMerge(
|
||||
"w-full",
|
||||
"twui-tab-buttons-wrapper",
|
||||
tabsButtonsWrapperProps?.className
|
||||
tabsButtonsWrapperProps?.className,
|
||||
)}
|
||||
>
|
||||
<Border
|
||||
@ -100,14 +133,14 @@ export default function Tabs({
|
||||
className={twMerge(
|
||||
"gap-0 items-stretch w-full flex-nowrap overflow-x-auto",
|
||||
centered && "justify-center",
|
||||
"twui-tab-buttons-container"
|
||||
"twui-tab-buttons-container",
|
||||
)}
|
||||
>
|
||||
{values.map((value, index) => {
|
||||
const targetObject = finalTabsContentArray.find(
|
||||
(ctn) =>
|
||||
ctn.value == value ||
|
||||
twuiSlugify(ctn.title) == value
|
||||
twuiSlugify(ctn.title) == value,
|
||||
);
|
||||
|
||||
const isActive = value == activeValue;
|
||||
@ -120,7 +153,7 @@ export default function Tabs({
|
||||
? "bg-primary dark:bg-primary-dark text-white outline-none twui-tab-button-active"
|
||||
: "text-slate-400 dark:text-white/40 hover:text-slate-800 dark:hover:text-white" +
|
||||
" cursor-pointer",
|
||||
"twui-tab-buttons"
|
||||
"twui-tab-buttons",
|
||||
)}
|
||||
onClick={() => {
|
||||
setActiveValue(undefined);
|
||||
|
||||
@ -14,6 +14,7 @@ export type TWUIToastProps = DetailedHTMLProps<
|
||||
> & {
|
||||
open?: boolean;
|
||||
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
closeDispatch?: (open?: boolean) => void;
|
||||
closeDelay?: number;
|
||||
color?: (typeof ToastStyles)[number];
|
||||
};
|
||||
@ -33,6 +34,7 @@ export default function Toast({
|
||||
setOpen,
|
||||
closeDelay = 4000,
|
||||
color,
|
||||
closeDispatch,
|
||||
...props
|
||||
}: TWUIToastProps) {
|
||||
const [ready, setReady] = React.useState(false);
|
||||
@ -56,10 +58,12 @@ export default function Toast({
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
setOpen?.(false);
|
||||
closeDispatch?.(open);
|
||||
}, closeDelay);
|
||||
|
||||
return function () {
|
||||
setOpen?.(false);
|
||||
closeDispatch?.(open);
|
||||
};
|
||||
}, [ready, open]);
|
||||
|
||||
@ -73,12 +77,12 @@ export default function Toast({
|
||||
"fixed bottom-4 right-4 z-[250] border-none",
|
||||
"pl-6 pr-8 py-4 bg-primary dark:bg-primary-dark",
|
||||
color == "success"
|
||||
? "bg-success dark:bg-success-dark twui-toast-success"
|
||||
? "bg-success-dark dark:bg-success-dark twui-toast-success"
|
||||
: color == "error"
|
||||
? "bg-error dark:bg-error-dark twui-toast-error"
|
||||
: "",
|
||||
? "bg-error dark:bg-error-dark twui-toast-error"
|
||||
: "",
|
||||
props.className,
|
||||
"twui-toast"
|
||||
"twui-toast",
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
window.clearTimeout(timeout);
|
||||
@ -86,22 +90,28 @@ export default function Toast({
|
||||
onMouseLeave={(e) => {
|
||||
timeout = setTimeout(() => {
|
||||
setOpen?.(false);
|
||||
closeDispatch?.(open);
|
||||
}, closeDelay);
|
||||
}}
|
||||
>
|
||||
<Span
|
||||
className={twMerge(
|
||||
"absolute top-2 right-2 z-[100] cursor-pointer",
|
||||
"text-white"
|
||||
"text-white",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setOpen?.(false);
|
||||
closeDispatch?.(open);
|
||||
}}
|
||||
>
|
||||
<X size={15} />
|
||||
</Span>
|
||||
<Span className={twMerge("text-white")}>{props.children}</Span>
|
||||
<Span className={twMerge("text-white! font-semibold")}>
|
||||
{props.children}
|
||||
</Span>
|
||||
</Card>,
|
||||
document.getElementById(IDName) as HTMLElement
|
||||
document.getElementById(IDName) as HTMLElement,
|
||||
);
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ export default function AIPromptHistoryModal({ history }: Props) {
|
||||
View History
|
||||
</Button>
|
||||
}
|
||||
className="max-w-[900px] bg-slate-100 dark:bg-white/5 xl:p-10"
|
||||
className="max-w-[900px] bg-slate-100 dark:bg-white/5 xl:p-8"
|
||||
>
|
||||
<Stack className="gap-10 w-full">
|
||||
<Stack className="gap-1">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Divider from "@/src/components/twui/layout/Divider";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import Divider from "../../layout/Divider";
|
||||
import Stack from "../../layout/Stack";
|
||||
import React from "react";
|
||||
import MarkdownEditorPreviewComponent from "@/src/components/twui/mdx/markdown/MarkdownEditorPreviewComponent";
|
||||
import MarkdownEditorPreviewComponent from "../../mdx/markdown/MarkdownEditorPreviewComponent";
|
||||
import { ChatCompletionMessageParam } from "openai/resources/index";
|
||||
|
||||
type Props = {
|
||||
|
||||
20
components/lib/elements/lucide-icon.tsx
Normal file
20
components/lib/elements/lucide-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import type { LucideProps } from "lucide-react";
|
||||
import * as icons from "lucide-react";
|
||||
import React from "react";
|
||||
|
||||
export type TWUILucideIconName = keyof typeof icons;
|
||||
|
||||
export type TWUILucideIconProps = LucideProps & {
|
||||
name: TWUILucideIconName;
|
||||
};
|
||||
|
||||
export default function LucideIcon({ name, ...props }: TWUILucideIconProps) {
|
||||
const IconComponent = icons[name] as any;
|
||||
|
||||
if (!IconComponent) {
|
||||
console.warn(`Lucide icon "${name}" not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return <IconComponent {...props} />;
|
||||
}
|
||||
@ -66,7 +66,7 @@ export default function Checkbox({
|
||||
const finalSize = size || 20;
|
||||
|
||||
const [checked, setChecked] = React.useState(
|
||||
defaultChecked || externalChecked || false
|
||||
defaultChecked || externalChecked || false,
|
||||
);
|
||||
|
||||
const finalTitle = title
|
||||
@ -93,7 +93,7 @@ export default function Checkbox({
|
||||
"flex items-start md:items-center gap-2 flex-wrap md:flex-nowrap",
|
||||
readOnly ? "opacity-70 pointer-events-none" : "",
|
||||
wrapperClassName,
|
||||
wrapperProps?.className
|
||||
wrapperProps?.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
setChecked(!checked);
|
||||
@ -108,7 +108,7 @@ export default function Checkbox({
|
||||
? "bg-primary twui-checkbox-checked text-white outline-slate-400"
|
||||
: "dark:outline-white/50 outline-2 -outline-offset-2 twui-checkbox-unchecked",
|
||||
"twui-checkbox",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
style={{
|
||||
minWidth: finalSize + "px",
|
||||
@ -125,7 +125,7 @@ export default function Checkbox({
|
||||
{...labelProps}
|
||||
className={twMerge(
|
||||
"select-none whitespace-normal md:whitespace-nowrap",
|
||||
labelProps?.className
|
||||
labelProps?.className,
|
||||
)}
|
||||
>
|
||||
{label || finalTitle}
|
||||
@ -134,8 +134,8 @@ export default function Checkbox({
|
||||
)}
|
||||
</div>
|
||||
{info && (
|
||||
<Row className="gap-1" title={info.toString()}>
|
||||
<Info size={12} className="opacity-40" />
|
||||
<Row className="gap-1 flex-nowrap" title={info.toString()}>
|
||||
<Info size={13} className="opacity-40 min-w-[20px]" />
|
||||
<Span size="smaller" className="opacity-70">
|
||||
{info}
|
||||
</Span>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import _ from "lodash";
|
||||
import { DetailedHTMLProps, FormHTMLAttributes } from "react";
|
||||
import { DetailedHTMLProps, FormHTMLAttributes, RefObject } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type Props<T extends { [key: string]: any } = { [key: string]: any }> =
|
||||
DetailedHTMLProps<FormHTMLAttributes<HTMLFormElement>, HTMLFormElement> & {
|
||||
submitHandler?: (e: React.FormEvent<HTMLFormElement>, data: T) => void;
|
||||
changeHandler?: (e: React.FormEvent<HTMLFormElement>, data: T) => void;
|
||||
formRef?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -13,8 +14,8 @@ type Props<T extends { [key: string]: any } = { [key: string]: any }> =
|
||||
* @className twui-form
|
||||
*/
|
||||
export default function Form<
|
||||
T extends { [key: string]: any } = { [key: string]: any }
|
||||
>({ ...props }: Props<T>) {
|
||||
T extends { [key: string]: any } = { [key: string]: any },
|
||||
>({ formRef, ...props }: Props<T>) {
|
||||
const finalProps = _.omit(props, ["submitHandler", "changeHandler"]);
|
||||
|
||||
return (
|
||||
@ -23,7 +24,7 @@ export default function Form<
|
||||
className={twMerge(
|
||||
"flex flex-col items-stretch gap-2 w-full bg-transparent",
|
||||
"twui-form",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@ -42,6 +43,7 @@ export default function Form<
|
||||
props.changeHandler?.(e, data);
|
||||
props.onChange?.(e);
|
||||
}}
|
||||
ref={formRef}
|
||||
>
|
||||
{props.children}
|
||||
</form>
|
||||
|
||||
@ -10,13 +10,15 @@ import imageInputToBase64, {
|
||||
} from "../utils/form/imageInputToBase64";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Tag from "../elements/Tag";
|
||||
import Input from "./Input";
|
||||
import Row from "../layout/Row";
|
||||
|
||||
type ImageUploadProps = DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
onChangeHandler?: (
|
||||
imgData: ImageInputToBase64FunctionReturn | undefined
|
||||
imgData: ImageInputToBase64FunctionReturn | undefined,
|
||||
) => any;
|
||||
fileInputProps?: DetailedHTMLProps<
|
||||
React.InputHTMLAttributes<HTMLInputElement>,
|
||||
@ -45,6 +47,7 @@ type ImageUploadProps = DetailedHTMLProps<
|
||||
React.SetStateAction<ImageInputToBase64FunctionReturn[] | undefined>
|
||||
>;
|
||||
setLoading?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setImgURL?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
externalImage?: ImageInputToBase64FunctionReturn;
|
||||
restoreImageFn?: () => void;
|
||||
};
|
||||
@ -67,6 +70,7 @@ export default function ImageUpload({
|
||||
multiple,
|
||||
restoreImageFn,
|
||||
setLoading,
|
||||
setImgURL,
|
||||
...props
|
||||
}: ImageUploadProps) {
|
||||
const [imageObject, setImageObject] = React.useState<
|
||||
@ -74,6 +78,11 @@ export default function ImageUpload({
|
||||
>(externalImage);
|
||||
const [src, setSrc] = React.useState<string | undefined>(existingImageUrl);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const imageUrlRef = React.useRef("");
|
||||
|
||||
React.useEffect(() => {
|
||||
setImgURL?.(src);
|
||||
}, [src]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (existingImageUrl) setSrc(existingImageUrl);
|
||||
@ -84,7 +93,7 @@ export default function ImageUpload({
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"w-full h-[300px] overflow-hidden",
|
||||
props?.className
|
||||
props?.className,
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@ -123,7 +132,7 @@ export default function ImageUpload({
|
||||
externalSetImage?.(res);
|
||||
fileInputProps?.onChange?.(e);
|
||||
setLoading?.(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}}
|
||||
@ -138,7 +147,7 @@ export default function ImageUpload({
|
||||
{label && (
|
||||
<label
|
||||
className={twMerge(
|
||||
"absolute top-0 left-0 text-xs z-50"
|
||||
"absolute top-0 left-0 text-xs z-50",
|
||||
)}
|
||||
>
|
||||
<Tag color="gray">
|
||||
@ -157,10 +166,10 @@ export default function ImageUpload({
|
||||
{...previewImageProps}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute p-1 top-2 right-2 z-20 bg-background-light dark:bg-background-dark"
|
||||
"absolute p-1 top-2 right-2 z-20 bg-background-light dark:bg-background-dark",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={(e) => {
|
||||
setSrc(undefined);
|
||||
@ -174,13 +183,13 @@ export default function ImageUpload({
|
||||
title="Cancel Image Upload Button"
|
||||
>
|
||||
<X className="text-slate-950 dark:text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
className={twMerge(
|
||||
"w-full h-full cursor-pointer hover:bg-slate-100 dark:hover:bg-white/20",
|
||||
placeHolderWrapper?.className
|
||||
placeHolderWrapper?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
const targetEl = e.target as HTMLElement | undefined;
|
||||
@ -199,6 +208,30 @@ export default function ImageUpload({
|
||||
<Span size="smaller" variant="faded">
|
||||
{label || "Click to Upload Image"}
|
||||
</Span>
|
||||
<Stack className="cancel-upload w-full items-stretch gap-1">
|
||||
<Input
|
||||
placeholder="Eg. https://example.com/img.png"
|
||||
className="text-sm twui-image-url-input"
|
||||
title="Enter Image URL"
|
||||
wrapperWrapperProps={{ className: "mt-2" }}
|
||||
changeHandler={(value) => {
|
||||
imageUrlRef.current = value;
|
||||
}}
|
||||
showLabel
|
||||
/>
|
||||
<Button
|
||||
title="Restore Image Button"
|
||||
size="smaller"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
onClick={() => {
|
||||
if (!imageUrlRef.current) return;
|
||||
setSrc(imageUrlRef.current);
|
||||
}}
|
||||
>
|
||||
Set Image URL
|
||||
</Button>
|
||||
</Stack>
|
||||
{existingImageUrl && (
|
||||
<Button
|
||||
title="Restore Image Button"
|
||||
|
||||
@ -8,7 +8,7 @@ let pressInterval: any;
|
||||
let pressTimeout: any;
|
||||
|
||||
type Props = Pick<InputProps<any>, "min" | "max" | "step"> & {
|
||||
updateValue: (v: string) => void;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
getNormalizedValue: (v: string) => void;
|
||||
buttonDownRef: React.MutableRefObject<boolean>;
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
@ -19,7 +19,7 @@ type Props = Pick<InputProps<any>, "min" | "max" | "step"> & {
|
||||
*/
|
||||
export default function NumberInputButtons({
|
||||
getNormalizedValue,
|
||||
updateValue,
|
||||
setValue,
|
||||
min,
|
||||
max,
|
||||
step,
|
||||
@ -65,12 +65,14 @@ export default function NumberInputButtons({
|
||||
const existingNumberValue = twuiNumberfy(existingValue);
|
||||
|
||||
if (max && existingNumberValue >= twuiNumberfy(max)) {
|
||||
return updateValue(String(max));
|
||||
return setValue(String(max));
|
||||
} else if (min && existingNumberValue < twuiNumberfy(min)) {
|
||||
return updateValue(String(min));
|
||||
return setValue(String(min));
|
||||
} else {
|
||||
updateValue(
|
||||
String(existingNumberValue + twuiNumberfy(step || DEFAULT_STEP))
|
||||
setValue(
|
||||
String(
|
||||
existingNumberValue + twuiNumberfy(step || DEFAULT_STEP),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -80,10 +82,12 @@ export default function NumberInputButtons({
|
||||
const existingNumberValue = twuiNumberfy(existingValue);
|
||||
|
||||
if (min && existingNumberValue <= twuiNumberfy(min)) {
|
||||
updateValue(String(min));
|
||||
setValue(String(min));
|
||||
} else {
|
||||
updateValue(
|
||||
String(existingNumberValue - twuiNumberfy(step || DEFAULT_STEP))
|
||||
setValue(
|
||||
String(
|
||||
existingNumberValue - twuiNumberfy(step || DEFAULT_STEP),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,13 +27,16 @@ let timeout: any;
|
||||
let validationFnTimeout: any;
|
||||
let externalValueChangeTimeout: any;
|
||||
|
||||
export type InputProps<KeyType extends string> = DetailedHTMLProps<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
export type InputProps<KeyType extends string> = Omit<
|
||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||
"prefix" | "suffix"
|
||||
> &
|
||||
DetailedHTMLProps<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
HTMLTextAreaElement
|
||||
Omit<
|
||||
DetailedHTMLProps<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
HTMLTextAreaElement
|
||||
>,
|
||||
"prefix" | "suffix"
|
||||
> & {
|
||||
label?: string;
|
||||
variant?: "normal" | "warning" | "error" | "inactive";
|
||||
@ -60,16 +63,14 @@ export type InputProps<KeyType extends string> = DetailedHTMLProps<
|
||||
invalidMessage?: string;
|
||||
validationFunction?: (
|
||||
value: string,
|
||||
element?: HTMLInputElement | HTMLTextAreaElement
|
||||
element?: HTMLInputElement | HTMLTextAreaElement,
|
||||
) => Promise<TWUISelectValidityObject>;
|
||||
changeHandler?: (
|
||||
value: string,
|
||||
element?: HTMLInputElement | HTMLTextAreaElement
|
||||
) => void;
|
||||
changeHandler?: (value: string) => void;
|
||||
autoComplete?: (typeof AutocompleteOptions)[number];
|
||||
name?: KeyType;
|
||||
valueUpdate?: string;
|
||||
numberText?: boolean;
|
||||
rawNumber?: boolean;
|
||||
setReady?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
decimal?: number;
|
||||
info?: string | ReactNode;
|
||||
@ -91,7 +92,7 @@ let refreshes = 0;
|
||||
* @className twui-clear-input-field-button
|
||||
*/
|
||||
export default function Input<KeyType extends string>(
|
||||
inputProps: InputProps<KeyType>
|
||||
inputProps: InputProps<KeyType>,
|
||||
) {
|
||||
const {
|
||||
label,
|
||||
@ -119,10 +120,12 @@ export default function Input<KeyType extends string>(
|
||||
changeHandler,
|
||||
validity: existingValidity,
|
||||
clearInputProps,
|
||||
rawNumber,
|
||||
...props
|
||||
} = inputProps;
|
||||
|
||||
function getFinalValue(v: any) {
|
||||
if (rawNumber) return twuiNumberfy(v);
|
||||
if (numberText) {
|
||||
return (
|
||||
twuiNumberfy(v, decimal).toLocaleString() +
|
||||
@ -141,16 +144,19 @@ export default function Input<KeyType extends string>(
|
||||
const [validity, setValidity] = React.useState<TWUISelectValidityObject>(
|
||||
existingValidity || {
|
||||
isValid: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
|
||||
const textAreaRef = componentRef || React.useRef<HTMLTextAreaElement>(null);
|
||||
const buttonDownRef = React.useRef(false);
|
||||
|
||||
const [value, setValue] = React.useState(
|
||||
props.defaultValue ? String(props.defaultValue) : "",
|
||||
);
|
||||
const [focus, setFocus] = React.useState(false);
|
||||
const [inputType, setInputType] = React.useState(
|
||||
numberText ? "text" : props.type
|
||||
numberText ? "text" : props.type,
|
||||
);
|
||||
|
||||
const DEFAULT_DEBOUNCE = 500;
|
||||
@ -180,23 +186,20 @@ export default function Input<KeyType extends string>(
|
||||
setValidity(existingValidity);
|
||||
}, [existingValidity]);
|
||||
|
||||
const updateValueFn = (
|
||||
val: string,
|
||||
el?: HTMLInputElement | HTMLTextAreaElement
|
||||
) => {
|
||||
const updateValueFn = (val: string) => {
|
||||
if (buttonDownRef.current) return;
|
||||
|
||||
if (changeHandler) {
|
||||
window.clearTimeout(externalValueChangeTimeout);
|
||||
externalValueChangeTimeout = setTimeout(() => {
|
||||
changeHandler(val, el);
|
||||
changeHandler(val);
|
||||
}, finalDebounce);
|
||||
}
|
||||
|
||||
if (typeof val == "string") {
|
||||
if (!val.match(/./)) {
|
||||
setValidity({ isValid: true });
|
||||
props.value = "";
|
||||
setValue("");
|
||||
if (istextarea && textAreaRef.current) {
|
||||
textAreaRef.current.value = "";
|
||||
} else if (inputRef?.current) {
|
||||
@ -223,7 +226,7 @@ export default function Input<KeyType extends string>(
|
||||
if (validationRegex && !validationRegex.test(val)) {
|
||||
return;
|
||||
}
|
||||
validationFunction(val, el).then((res) => {
|
||||
validationFunction(val).then((res) => {
|
||||
setValidity(res);
|
||||
});
|
||||
}, finalDebounce);
|
||||
@ -233,28 +236,36 @@ export default function Input<KeyType extends string>(
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof props.value !== "string" || !props.value.match(/./)) return;
|
||||
updateValueFn(String(props.value));
|
||||
setValue(String(props.value));
|
||||
}, [props.value]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (istextarea && textAreaRef.current) {
|
||||
} else if (inputRef?.current) {
|
||||
inputRef.current.value = getFinalValue(value);
|
||||
}
|
||||
updateValueFn(value);
|
||||
}, [value]);
|
||||
|
||||
function handleValueChange(
|
||||
e: React.ChangeEvent<HTMLInputElement> &
|
||||
React.ChangeEvent<HTMLTextAreaElement>
|
||||
React.ChangeEvent<HTMLTextAreaElement>,
|
||||
) {
|
||||
const newValue = e.target.value;
|
||||
updateValue(newValue, e.target);
|
||||
setValue(newValue);
|
||||
props.onChange?.(e);
|
||||
}
|
||||
|
||||
function updateValue(
|
||||
v: string,
|
||||
el?: HTMLInputElement | HTMLTextAreaElement
|
||||
) {
|
||||
if (istextarea && textAreaRef.current) {
|
||||
} else if (inputRef?.current) {
|
||||
inputRef.current.value = getFinalValue(v);
|
||||
}
|
||||
updateValueFn(v, el);
|
||||
}
|
||||
// function updateValue(
|
||||
// v: string,
|
||||
// el?: HTMLInputElement | HTMLTextAreaElement,
|
||||
// ) {
|
||||
// if (istextarea && textAreaRef.current) {
|
||||
// } else if (inputRef?.current) {
|
||||
// inputRef.current.value = getFinalValue(v);
|
||||
// }
|
||||
// updateValueFn(v);
|
||||
// }
|
||||
|
||||
const targetComponent = istextarea ? (
|
||||
<textarea
|
||||
@ -265,7 +276,7 @@ export default function Input<KeyType extends string>(
|
||||
className={twMerge(
|
||||
"w-full outline-none bg-transparent grow",
|
||||
"twui-textarea",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
ref={textAreaRef}
|
||||
onFocus={(e) => {
|
||||
@ -294,7 +305,7 @@ export default function Input<KeyType extends string>(
|
||||
"dark:bg-transparent dark:outline-none dark:border-none",
|
||||
"p-0 grow",
|
||||
"twui-input",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
ref={inputRef}
|
||||
onFocus={(e) => {
|
||||
@ -318,8 +329,8 @@ export default function Input<KeyType extends string>(
|
||||
title={`${finalLabel}${props.required ? " (Required)" : ""}`}
|
||||
{...wrapperWrapperProps}
|
||||
className={twMerge(
|
||||
"w-full gap-1.5 relative z-0 hover:z-10",
|
||||
wrapperWrapperProps?.className
|
||||
"w-full gap-1.5 relative z-0 hover:z-100",
|
||||
wrapperWrapperProps?.className,
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@ -353,7 +364,7 @@ export default function Input<KeyType extends string>(
|
||||
: "opacity-50 pointer-events-none"
|
||||
: undefined,
|
||||
"twui-input-wrapper",
|
||||
wrapperProps?.className
|
||||
wrapperProps?.className,
|
||||
)}
|
||||
>
|
||||
{showLabel && (
|
||||
@ -365,7 +376,7 @@ export default function Input<KeyType extends string>(
|
||||
"dark:text-foreground-dark/80 dark:bg-background-dark whitespace-nowrap",
|
||||
"overflow-hidden overflow-ellipsis z-20 px-1.5 rounded-t-default",
|
||||
"twui-input-label",
|
||||
labelProps?.className
|
||||
labelProps?.className,
|
||||
)}
|
||||
>
|
||||
{finalLabel}
|
||||
@ -378,11 +389,7 @@ export default function Input<KeyType extends string>(
|
||||
</label>
|
||||
)}
|
||||
|
||||
{prefix && (
|
||||
<div className="opacity-60 pointer-events-none whitespace-nowrap">
|
||||
{prefix}
|
||||
</div>
|
||||
)}
|
||||
{prefix && prefix}
|
||||
|
||||
{targetComponent}
|
||||
|
||||
@ -394,7 +401,7 @@ export default function Input<KeyType extends string>(
|
||||
"p-1 -my-2 -mx-1 opacity-0 cursor-pointer w-7 h-7",
|
||||
"bg-background-light dark:bg-background-dark",
|
||||
"twui-clear-input-field-button",
|
||||
clearInputProps?.className
|
||||
clearInputProps?.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@ -406,7 +413,7 @@ export default function Input<KeyType extends string>(
|
||||
textAreaRef.current.value = "";
|
||||
}
|
||||
|
||||
updateValue("");
|
||||
setValue("");
|
||||
clearInputProps?.onClick?.(e);
|
||||
}}
|
||||
>
|
||||
@ -439,21 +446,11 @@ export default function Input<KeyType extends string>(
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{suffix ? (
|
||||
<div
|
||||
{...suffixProps}
|
||||
className={twMerge(
|
||||
"opacity-60 pointer-events-none whitespace-nowrap",
|
||||
suffixProps?.className
|
||||
)}
|
||||
>
|
||||
{suffix}
|
||||
</div>
|
||||
) : null}
|
||||
{suffix ? suffix : null}
|
||||
|
||||
{numberText ? (
|
||||
<NumberInputButtons
|
||||
updateValue={updateValue}
|
||||
setValue={setValue}
|
||||
inputRef={inputRef}
|
||||
getNormalizedValue={getNormalizedValue}
|
||||
max={props.max}
|
||||
@ -468,18 +465,22 @@ export default function Input<KeyType extends string>(
|
||||
target={
|
||||
<Row className="gap-1">
|
||||
<Info size={12} className="opacity-40" />
|
||||
<Span size="smaller" className="opacity-70">
|
||||
<Span
|
||||
size="smaller"
|
||||
className="opacity-70 hover:opacity-100"
|
||||
>
|
||||
{info}
|
||||
</Span>
|
||||
</Row>
|
||||
}
|
||||
openDebounce={700}
|
||||
className="z-1000"
|
||||
hoverOpen
|
||||
>
|
||||
<Paper
|
||||
className={twMerge(
|
||||
"min-w-[250px] shadow-lg shadow-slate-200 dark:shadow-white/10",
|
||||
"max-w-[300px] w-full"
|
||||
"max-w-[300px] w-full bg-background-light! dark:bg-background-dark! z-1000",
|
||||
)}
|
||||
>
|
||||
<Stack className="gap-2 items-center">
|
||||
|
||||
@ -6,7 +6,7 @@ type Params = {
|
||||
|
||||
let timeout: any;
|
||||
|
||||
export default function twuiUseReady(params?: Params) {
|
||||
export default function useReady(params?: Params) {
|
||||
const [ready, setReady] = React.useState(false);
|
||||
|
||||
const finalTimeout = params?.timeout || 300;
|
||||
|
||||
31
components/lib/hooks/useStatus.tsx
Normal file
31
components/lib/hooks/useStatus.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from "react";
|
||||
|
||||
type Params = {
|
||||
initialLoading?: boolean;
|
||||
initialReady?: boolean;
|
||||
};
|
||||
|
||||
export type UseStatusStatusType = {
|
||||
msg?: string;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
export default function useStatus(params?: Params) {
|
||||
const [refresh, setRefresh] = React.useState(0);
|
||||
const [loading, setLoading] = React.useState(
|
||||
params?.initialLoading || false
|
||||
);
|
||||
const [status, setStatus] = React.useState<UseStatusStatusType>({});
|
||||
const [ready, setReady] = React.useState(params?.initialReady || false);
|
||||
|
||||
return {
|
||||
refresh,
|
||||
setRefresh,
|
||||
loading,
|
||||
setLoading,
|
||||
status,
|
||||
setStatus,
|
||||
ready,
|
||||
setReady,
|
||||
};
|
||||
}
|
||||
@ -1,17 +1,16 @@
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
|
||||
export type UseWebsocketHookParams = {
|
||||
debounce?: number;
|
||||
url: string;
|
||||
disableReconnect?: boolean;
|
||||
/** Interval to ping the websocket. So that the connection doesn't go down. Default 30000ms (30 seconds) */
|
||||
keepAliveDuration?: number;
|
||||
refreshConnection?: number;
|
||||
};
|
||||
|
||||
export const WebSocketEventNames = ["wsDataEvent", "wsMessageEvent"] as const;
|
||||
|
||||
let tries = 0;
|
||||
|
||||
/**
|
||||
* # Use Websocket Hook
|
||||
* @event wsDataEvent Listen for event named `wsDataEvent` on `window` to receive Data events
|
||||
@ -33,18 +32,20 @@ export default function useWebSocket<
|
||||
keepAliveDuration,
|
||||
refreshConnection,
|
||||
}: UseWebsocketHookParams) {
|
||||
const DEBOUNCE = debounce || 200;
|
||||
const DEBOUNCE = debounce || 500;
|
||||
const KEEP_ALIVE_DURATION = keepAliveDuration || 1000 * 30;
|
||||
const KEEP_ALIVE_TIMEOUT = 1000 * 60 * 3;
|
||||
|
||||
const KEEP_ALIVE_MESSAGE = "twui::ping";
|
||||
|
||||
let uptime = 0;
|
||||
let tries = useRef(0);
|
||||
|
||||
let reconnectInterval: any;
|
||||
let msgInterval: any;
|
||||
let sendInterval: any;
|
||||
let keepAliveInterval: any;
|
||||
// const queue: string[] = [];
|
||||
|
||||
const msgInterval = useRef<any>(null);
|
||||
const sendInterval = useRef<any>(null);
|
||||
const keepAliveInterval = useRef<any>(null);
|
||||
|
||||
const [socket, setSocket] = React.useState<WebSocket | undefined>(
|
||||
undefined
|
||||
@ -77,141 +78,94 @@ export default function useWebSocket<
|
||||
const wsURL = url.startsWith(`ws`)
|
||||
? url
|
||||
: domain.replace(/^http/, "ws") + ("/" + url).replace(/\/\//g, "/");
|
||||
|
||||
if (!wsURL) return;
|
||||
|
||||
let ws = new WebSocket(wsURL);
|
||||
|
||||
ws.onopen = (ev) => {
|
||||
window.clearInterval(reconnectInterval);
|
||||
window.clearInterval(keepAliveInterval);
|
||||
keepAliveInterval = setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(KEEP_ALIVE_MESSAGE);
|
||||
uptime += KEEP_ALIVE_DURATION;
|
||||
if (uptime >= KEEP_ALIVE_TIMEOUT) {
|
||||
console.log("Websocket connection timed out ...");
|
||||
window.clearInterval(keepAliveInterval);
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
}, KEEP_ALIVE_DURATION);
|
||||
setSocket(ws);
|
||||
tries = 0;
|
||||
console.log(`Websocket connected to ${wsURL}`);
|
||||
uptime = 0;
|
||||
ws.onerror = (ev) => {
|
||||
console.log(`Websocket ERROR:`);
|
||||
};
|
||||
|
||||
ws.onmessage = (ev) => {
|
||||
window.clearInterval(msgInterval);
|
||||
messageQueueRef.current.push(ev.data);
|
||||
msgInterval = setInterval(handleReceivedMessageQueue, DEBOUNCE);
|
||||
if (ev.data !== KEEP_ALIVE_MESSAGE) {
|
||||
uptime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
ws.onopen = (ev) => {
|
||||
window.clearInterval(keepAliveInterval.current);
|
||||
|
||||
keepAliveInterval.current = window.setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(KEEP_ALIVE_MESSAGE);
|
||||
}
|
||||
}, KEEP_ALIVE_DURATION);
|
||||
|
||||
setSocket(ws);
|
||||
console.log(`Websocket connected to ${wsURL}`);
|
||||
};
|
||||
|
||||
ws.onclose = (ev) => {
|
||||
console.log("Websocket closed!");
|
||||
console.log("Websocket closed!", {
|
||||
code: ev.code,
|
||||
reason: ev.reason,
|
||||
wasClean: ev.wasClean,
|
||||
});
|
||||
|
||||
if (disableReconnect) return;
|
||||
|
||||
console.log("Attempting to reconnect ...");
|
||||
console.log("URL:", url);
|
||||
window.clearInterval(keepAliveInterval);
|
||||
window.clearInterval(keepAliveInterval.current);
|
||||
|
||||
reconnectInterval = setInterval(() => {
|
||||
if (tries >= 3) {
|
||||
return window.clearInterval(reconnectInterval);
|
||||
}
|
||||
console.log("tries", tries);
|
||||
|
||||
console.log("Attempting to reconnect ...");
|
||||
tries++;
|
||||
if (tries.current >= 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
connect();
|
||||
}, 1000);
|
||||
console.log("Attempting to reconnect ...");
|
||||
|
||||
tries.current += 1;
|
||||
|
||||
connect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* # Window Close Handler
|
||||
*/
|
||||
const handleWindowClose = React.useCallback(() => {
|
||||
console.log("Window Unloaded ...");
|
||||
}, [socket]);
|
||||
|
||||
/**
|
||||
* # Window Focus Handler
|
||||
*/
|
||||
const handleWindowFocus = React.useCallback(() => {
|
||||
if (socket?.readyState === WebSocket.CLOSED) {
|
||||
console.log("Websocket closed ... Attempting to reconnect ...");
|
||||
connect();
|
||||
}
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
console.log("Websocket connection alive ...");
|
||||
socket.send(KEEP_ALIVE_MESSAGE);
|
||||
uptime = 0;
|
||||
}
|
||||
}, [socket]);
|
||||
|
||||
/**
|
||||
* # Initial Connection
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
connect();
|
||||
if (socket) return;
|
||||
|
||||
return function () {
|
||||
window.clearInterval(reconnectInterval);
|
||||
};
|
||||
connect();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* # Window Close and Focus Handlers
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!socket) return;
|
||||
|
||||
window.addEventListener("beforeunload", handleWindowClose, {
|
||||
once: true,
|
||||
});
|
||||
window.addEventListener("focus", handleWindowFocus);
|
||||
sendInterval.current = setInterval(handleSendMessageQueue, DEBOUNCE);
|
||||
msgInterval.current = setInterval(handleReceivedMessageQueue, DEBOUNCE);
|
||||
|
||||
return function () {
|
||||
window.removeEventListener("focus", handleWindowFocus);
|
||||
window.removeEventListener("beforeunload", handleWindowClose);
|
||||
window.clearInterval(sendInterval.current);
|
||||
window.clearInterval(msgInterval.current);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
/**
|
||||
* # Refresh Connection
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
console.log("Refreshing connection ...");
|
||||
|
||||
if (!socket) return;
|
||||
if (socket.readyState !== WebSocket.CLOSED) {
|
||||
socket?.close();
|
||||
}
|
||||
connect();
|
||||
}, [refreshConnection]);
|
||||
|
||||
/**
|
||||
* Received Message Queue Handler
|
||||
*/
|
||||
const handleReceivedMessageQueue = React.useCallback(() => {
|
||||
if (messageQueueRef.current.length > 0) {
|
||||
const newMessage = messageQueueRef.current.shift();
|
||||
if (!newMessage) return;
|
||||
try {
|
||||
const jsonData = JSON.parse(newMessage);
|
||||
dispatchCustomEvent("wsMessageEvent", newMessage);
|
||||
dispatchCustomEvent("wsDataEvent", jsonData);
|
||||
} catch (error) {
|
||||
console.log("Unable to parse string. Returning string.");
|
||||
}
|
||||
} else {
|
||||
window.clearInterval(msgInterval);
|
||||
uptime = 0;
|
||||
try {
|
||||
const msg = messageQueueRef.current.shift();
|
||||
|
||||
if (!msg) return;
|
||||
|
||||
const jsonData = JSON.parse(msg);
|
||||
dispatchCustomEvent("wsMessageEvent", msg);
|
||||
dispatchCustomEvent("wsDataEvent", jsonData);
|
||||
} catch (error) {
|
||||
console.log("Unable to parse string. Returning string.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
@ -219,13 +173,15 @@ export default function useWebSocket<
|
||||
* Send Message Queue Handler
|
||||
*/
|
||||
const handleSendMessageQueue = React.useCallback(() => {
|
||||
if (sendMessageQueueRef.current.length > 0) {
|
||||
const newMessage = sendMessageQueueRef.current.shift();
|
||||
if (!newMessage) return;
|
||||
socket?.send(newMessage);
|
||||
} else {
|
||||
window.clearInterval(sendInterval);
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) {
|
||||
window.clearInterval(sendInterval.current);
|
||||
return;
|
||||
}
|
||||
|
||||
const newMessage = sendMessageQueueRef.current.shift();
|
||||
if (!newMessage) return;
|
||||
|
||||
socket.send(newMessage);
|
||||
}, [socket]);
|
||||
|
||||
/**
|
||||
@ -234,9 +190,14 @@ export default function useWebSocket<
|
||||
const sendData = React.useCallback(
|
||||
(data: T) => {
|
||||
try {
|
||||
window.clearInterval(sendInterval);
|
||||
sendMessageQueueRef.current.push(JSON.stringify(data));
|
||||
sendInterval = setInterval(handleSendMessageQueue, DEBOUNCE);
|
||||
const queueItemJSON = JSON.stringify(data);
|
||||
|
||||
const existingQueue = sendMessageQueueRef.current.find(
|
||||
(q) => q == queueItemJSON
|
||||
);
|
||||
if (!existingQueue) {
|
||||
sendMessageQueueRef.current.push(queueItemJSON);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log("Error Sending socket message", error.message);
|
||||
}
|
||||
|
||||
24
components/lib/hooks/userWindowFocus.tsx
Normal file
24
components/lib/hooks/userWindowFocus.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export default function useWindowFocus() {
|
||||
const [isWindowFocused, setIsWindowFocused] = useState(false);
|
||||
|
||||
const windowFocusCb = useCallback(() => {
|
||||
setIsWindowFocused(true);
|
||||
}, []);
|
||||
|
||||
const windowBlurCb = useCallback(() => {
|
||||
setIsWindowFocused(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("focus", windowFocusCb);
|
||||
window.addEventListener("blur", windowBlurCb);
|
||||
return function () {
|
||||
window.removeEventListener("focus", windowFocusCb);
|
||||
window.removeEventListener("blur", windowBlurCb);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isWindowFocused };
|
||||
}
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Loading from "../elements/Loading";
|
||||
import LucideIcon, { TWUILucideIconName } from "../elements/lucide-icon";
|
||||
|
||||
export type TWUIButtonProps = DetailedHTMLProps<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
@ -34,8 +35,8 @@ export type TWUIButtonProps = DetailedHTMLProps<
|
||||
AnchorHTMLAttributes<HTMLAnchorElement>,
|
||||
HTMLAnchorElement
|
||||
>;
|
||||
beforeIcon?: React.ReactNode;
|
||||
afterIcon?: React.ReactNode;
|
||||
beforeIcon?: TWUILucideIconName | JSX.Element;
|
||||
afterIcon?: TWUILucideIconName | JSX.Element;
|
||||
buttonContentProps?: DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
@ -117,132 +118,132 @@ export default function Button({
|
||||
return twMerge(
|
||||
"bg-primary hover:bg-primary-hover text-white",
|
||||
"dark:bg-primary-dark hover:dark:bg-primary-dark-hover text-white",
|
||||
"twui-button-primary"
|
||||
"twui-button-primary",
|
||||
);
|
||||
if (color == "secondary")
|
||||
return twMerge(
|
||||
"bg-secondary hover:bg-secondary-hover text-white",
|
||||
"twui-button-secondary"
|
||||
"twui-button-secondary",
|
||||
);
|
||||
if (color == "white")
|
||||
return twMerge(
|
||||
"!bg-white hover:!bg-slate-200 !text-slate-800",
|
||||
"twui-button-white"
|
||||
"twui-button-white",
|
||||
);
|
||||
if (color == "accent")
|
||||
return twMerge(
|
||||
"bg-accent hover:bg-accent-hover text-white",
|
||||
"twui-button-accent"
|
||||
"twui-button-accent",
|
||||
);
|
||||
if (color == "gray")
|
||||
return twMerge(
|
||||
"bg-gray hover:bg-gray-hover text-foreground-light",
|
||||
"dark:bg-gray-dark hover:dark:bg-gray-dark-hover dark:text-foreground-dark",
|
||||
"twui-button-gray"
|
||||
"twui-button-gray",
|
||||
);
|
||||
if (color == "success")
|
||||
return twMerge(
|
||||
"bg-success hover:bg-success-hover text-white",
|
||||
"dark:bg-success hover:dark:bg-success-hover text-white",
|
||||
"twui-button-success"
|
||||
"twui-button-success",
|
||||
);
|
||||
if (color == "error")
|
||||
return twMerge(
|
||||
"bg-error hover:bg-error-hover text-white",
|
||||
"dark:bg-error hover:dark:bg-error-hover text-white",
|
||||
"twui-button-error"
|
||||
"twui-button-error",
|
||||
);
|
||||
} else if (variant == "outlined") {
|
||||
if (color == "primary" || !color)
|
||||
return twMerge(
|
||||
"bg-transparent outline outline-1 outline-primary",
|
||||
"text-primary-text dark:text-primary-dark-text dark:outline-primary-dark-outline",
|
||||
"twui-button-primary-outlined"
|
||||
"twui-button-primary-outlined",
|
||||
);
|
||||
if (color == "secondary")
|
||||
return twMerge(
|
||||
"bg-transparent outline outline-1 outline-secondary",
|
||||
"text-secondary",
|
||||
"twui-button-secondary-outlined"
|
||||
"twui-button-secondary-outlined",
|
||||
);
|
||||
if (color == "accent")
|
||||
return twMerge(
|
||||
"bg-transparent outline outline-1 outline-accent",
|
||||
"text-accent",
|
||||
"twui-button-accent-outlined"
|
||||
"twui-button-accent-outlined",
|
||||
);
|
||||
if (color == "gray")
|
||||
return twMerge(
|
||||
"bg-transparent outline outline-1 outline-slate-300",
|
||||
"text-slate-600 dark:text-white/60 dark:outline-white/30",
|
||||
"twui-button-gray-outlined"
|
||||
"twui-button-gray-outlined",
|
||||
);
|
||||
if (color == "white")
|
||||
return twMerge(
|
||||
"bg-transparent outline outline-1 outline-white/50",
|
||||
"text-white",
|
||||
"twui-button-white-outlined"
|
||||
"twui-button-white-outlined",
|
||||
);
|
||||
if (color == "error")
|
||||
return twMerge(
|
||||
"bg-transparent outline outline-1 outline-error text-error",
|
||||
"dark:outline-error dark:text-error-dark",
|
||||
"twui-button-error-outlined"
|
||||
"twui-button-error-outlined",
|
||||
);
|
||||
} else if (variant == "ghost") {
|
||||
if (color == "primary" || !color)
|
||||
return twMerge(
|
||||
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||
"text-primary-text dark:text-primary-dark-text hover:bg-transparent dark:hover:bg-transparent",
|
||||
"twui-button-primary-ghost"
|
||||
"twui-button-primary-ghost",
|
||||
);
|
||||
if (color == "secondary")
|
||||
return twMerge(
|
||||
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||
"text-secondary hover:bg-transparent dark:hover:bg-transparent",
|
||||
"twui-button-secondary-ghost"
|
||||
"twui-button-secondary-ghost",
|
||||
);
|
||||
if (color == "text")
|
||||
return twMerge(
|
||||
"bg-transparent dark:bg-transparent outline-none p-2 dark:text-foreground-dark",
|
||||
"text-foreground-light hover:bg-transparent dark:hover:bg-transparent",
|
||||
"twui-button-secondary-ghost"
|
||||
"twui-button-secondary-ghost",
|
||||
);
|
||||
if (color == "accent")
|
||||
return twMerge(
|
||||
"bg-transparent dark:bg-transparent outline-none p-2",
|
||||
"text-accent hover:bg-transparent dark:hover:bg-transparent",
|
||||
"twui-button-accent-ghost"
|
||||
"twui-button-accent-ghost",
|
||||
);
|
||||
if (color == "gray")
|
||||
return twMerge(
|
||||
"bg-transparent dark:bg-transparent outline-none p-2 hover:bg-transparent dark:hover:bg-transparent",
|
||||
"text-slate-600 dark:text-white/70 hover:opacity-80",
|
||||
"twui-button-gray-ghost"
|
||||
"twui-button-gray-ghost",
|
||||
);
|
||||
if (color == "error")
|
||||
return twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-red-600 dark:text-red-400",
|
||||
"twui-button-error-ghost"
|
||||
"twui-button-error-ghost",
|
||||
);
|
||||
if (color == "warning")
|
||||
return twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-yellow-600",
|
||||
"twui-button-warning-ghost"
|
||||
"twui-button-warning-ghost",
|
||||
);
|
||||
if (color == "success")
|
||||
return twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-success",
|
||||
"twui-button-success-ghost"
|
||||
"twui-button-success-ghost",
|
||||
);
|
||||
if (color == "white")
|
||||
return twMerge(
|
||||
"bg-transparent outline-none p-2",
|
||||
"text-white",
|
||||
"twui-button-white-ghost"
|
||||
"twui-button-white-ghost",
|
||||
);
|
||||
}
|
||||
|
||||
@ -260,15 +261,15 @@ export default function Button({
|
||||
size == "small"
|
||||
? "px-3 py-1.5 twui-button-small text-sm"
|
||||
: size == "smaller"
|
||||
? "px-2 py-1 text-xs twui-button-smaller"
|
||||
: size == "large"
|
||||
? "text-lg twui-button-large"
|
||||
: size == "larger"
|
||||
? "px-5 py-3 text-xl twui-button-larger"
|
||||
: "twui-button-base",
|
||||
? "px-2 py-1 text-xs twui-button-smaller"
|
||||
: size == "large"
|
||||
? "text-lg twui-button-large"
|
||||
: size == "larger"
|
||||
? "px-5 py-3 text-xl twui-button-larger"
|
||||
: "twui-button-base",
|
||||
finalClassName,
|
||||
loading ? "pointer-events-none opacity-80" : "",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
aria-label={props.title}
|
||||
>
|
||||
@ -278,12 +279,24 @@ export default function Button({
|
||||
"flex flex-row items-center gap-2 whitespace-nowrap",
|
||||
loading ? "opacity-0" : "",
|
||||
"twui-button-content-wrapper",
|
||||
buttonContentProps?.className
|
||||
buttonContentProps?.className,
|
||||
)}
|
||||
>
|
||||
{beforeIcon && beforeIcon}
|
||||
{beforeIcon ? (
|
||||
typeof beforeIcon == "string" ? (
|
||||
<LucideIcon name={beforeIcon as TWUILucideIconName} />
|
||||
) : (
|
||||
beforeIcon
|
||||
)
|
||||
) : null}
|
||||
{props.children}
|
||||
{afterIcon && afterIcon}
|
||||
{afterIcon ? (
|
||||
typeof afterIcon == "string" ? (
|
||||
<LucideIcon name={afterIcon as TWUILucideIconName} />
|
||||
) : (
|
||||
afterIcon
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
|
||||
@ -9,6 +9,7 @@ export type LoadingRectangleBlockProps = DetailedHTMLProps<
|
||||
/**
|
||||
* # A loading Rectangle block
|
||||
* @className twui-loading-rectangle-block
|
||||
* @className twui-loading-block
|
||||
*/
|
||||
export default function LoadingRectangleBlock({
|
||||
...props
|
||||
@ -19,8 +20,8 @@ export default function LoadingRectangleBlock({
|
||||
className={twMerge(
|
||||
"flex items-center w-full h-10 animate-pulse bg-slate-200 rounded",
|
||||
"dark:bg-slate-800",
|
||||
"twui-loading-rectangle-block",
|
||||
props.className
|
||||
"twui-loading-rectangle-block twui-loading-block",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@ -8,10 +8,12 @@ import { twMerge } from "tailwind-merge";
|
||||
export default function Span({
|
||||
size,
|
||||
variant,
|
||||
truncate,
|
||||
...props
|
||||
}: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement> & {
|
||||
size?: "normal" | "small" | "smaller" | "large" | "larger";
|
||||
variant?: "normal" | "faded";
|
||||
truncate?: { lines?: number; width?: number };
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
@ -23,8 +25,9 @@ export default function Span({
|
||||
size == "large" && "text-lg",
|
||||
size == "larger" && "text-xl",
|
||||
variant == "faded" && "opacity-50",
|
||||
truncate ? `` : ``,
|
||||
"twui-span",
|
||||
props.className
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@ -48,9 +48,7 @@ export default function MarkdownEditorPreviewComponent({
|
||||
.then((mdxSrc) => {
|
||||
setMdxSource(mdxSrc);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`Markdown Parsing Error => ${err.message}`);
|
||||
});
|
||||
.catch((err) => {});
|
||||
} catch (error) {}
|
||||
}, [value]);
|
||||
|
||||
|
||||
38
components/lib/utils/ejson.ts
Normal file
38
components/lib/utils/ejson.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* # EJSON parse string
|
||||
*/
|
||||
function parse(
|
||||
string: string | null | number,
|
||||
reviver?: (this: any, key: string, value: any) => any,
|
||||
): { [s: string]: any } | { [s: string]: any }[] | undefined {
|
||||
if (!string) return undefined;
|
||||
if (typeof string == "object") return string;
|
||||
if (typeof string !== "string") return undefined;
|
||||
try {
|
||||
return JSON.parse(string, reviver);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* # EJSON stringify object
|
||||
*/
|
||||
function stringify(
|
||||
value: any,
|
||||
replacer?: ((this: any, key: string, value: any) => any) | null,
|
||||
space?: string | number,
|
||||
): string | undefined {
|
||||
try {
|
||||
return JSON.stringify(value, replacer || undefined, space);
|
||||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const TWUIEJSON = {
|
||||
parse,
|
||||
stringify,
|
||||
};
|
||||
|
||||
export default TWUIEJSON;
|
||||
@ -1,4 +1,5 @@
|
||||
import _ from "lodash";
|
||||
import twuiSerializeQuery from "../serialize-query";
|
||||
|
||||
export const FetchAPIMethods = [
|
||||
"POST",
|
||||
@ -17,6 +18,10 @@ type FetchApiOptions<T extends { [k: string]: any } = { [k: string]: any }> = {
|
||||
method: (typeof FetchAPIMethods)[number];
|
||||
body?: T | string;
|
||||
headers?: FetchHeader;
|
||||
query?: T;
|
||||
csrfValue?: string;
|
||||
csrfKey?: string;
|
||||
fetchOptions?: RequestInit;
|
||||
};
|
||||
|
||||
type FetchHeader = HeadersInit & {
|
||||
@ -35,32 +40,22 @@ export type FetchApiReturn = {
|
||||
*/
|
||||
export default async function fetchApi<
|
||||
T extends { [k: string]: any } = { [k: string]: any },
|
||||
R extends any = any
|
||||
>(
|
||||
url: string,
|
||||
options?: FetchApiOptions<T>,
|
||||
csrf?: boolean,
|
||||
/**
|
||||
* Key to use to grab local Storage csrf value.
|
||||
*/
|
||||
localStorageCSRFKey?: string,
|
||||
/**
|
||||
* Key with which to set the request header csrf
|
||||
* value
|
||||
*/
|
||||
csrfHeaderKey?: string
|
||||
): Promise<R> {
|
||||
R extends any = any,
|
||||
>(url: string, options?: FetchApiOptions<T>): Promise<R> {
|
||||
let data;
|
||||
|
||||
const csrfKey = "x-dsql-csrf-key";
|
||||
const csrfValue = localStorage.getItem(localStorageCSRFKey || csrfKey);
|
||||
|
||||
let finalHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
} as FetchHeader;
|
||||
|
||||
if (csrf && csrfValue) {
|
||||
finalHeaders[localStorageCSRFKey || csrfKey] = csrfValue;
|
||||
if (options?.csrfKey && options.csrfValue) {
|
||||
finalHeaders[options.csrfKey] = options.csrfValue;
|
||||
}
|
||||
|
||||
let finalURL = url;
|
||||
|
||||
if (options?.query) {
|
||||
finalURL += twuiSerializeQuery(options.query);
|
||||
}
|
||||
|
||||
if (typeof options === "string") {
|
||||
@ -69,7 +64,7 @@ export default async function fetchApi<
|
||||
|
||||
switch (options) {
|
||||
case "post":
|
||||
fetchData = await fetch(url, {
|
||||
fetchData = await fetch(finalURL, {
|
||||
method: options,
|
||||
headers: finalHeaders,
|
||||
} as RequestInit);
|
||||
@ -77,7 +72,7 @@ export default async function fetchApi<
|
||||
break;
|
||||
|
||||
default:
|
||||
fetchData = await fetch(url);
|
||||
fetchData = await fetch(finalURL);
|
||||
data = fetchData.json();
|
||||
break;
|
||||
}
|
||||
@ -98,14 +93,14 @@ export default async function fetchApi<
|
||||
options.headers = _.merge(options.headers, finalHeaders);
|
||||
|
||||
const finalOptions: any = { ...options };
|
||||
fetchData = await fetch(url, finalOptions);
|
||||
fetchData = await fetch(finalURL, finalOptions);
|
||||
} else {
|
||||
const finalOptions = {
|
||||
...options,
|
||||
headers: finalHeaders,
|
||||
} as RequestInit;
|
||||
|
||||
fetchData = await fetch(url, finalOptions);
|
||||
fetchData = await fetch(finalURL, finalOptions);
|
||||
}
|
||||
|
||||
data = fetchData.json();
|
||||
@ -115,7 +110,7 @@ export default async function fetchApi<
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let fetchData = await fetch(url);
|
||||
let fetchData = await fetch(finalURL);
|
||||
data = await fetchData.json();
|
||||
} catch (error: any) {
|
||||
console.log("FetchAPI error #3:", error.message);
|
||||
|
||||
@ -25,7 +25,6 @@ export default function twuiNumberfy(num: any, decimals?: number): number {
|
||||
return Number(numberfiedNum.toFixed(existingDecimals));
|
||||
return Math.round(numberfiedNum);
|
||||
} catch (error: any) {
|
||||
console.log(`Numberfy ERROR: ${error.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
42
components/lib/utils/serialize-query.ts
Normal file
42
components/lib/utils/serialize-query.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import TWUIEJSON from "./ejson";
|
||||
|
||||
/**
|
||||
* # Serialize Query
|
||||
*/
|
||||
export default function twuiSerializeQuery(query: any): string {
|
||||
let str = "?";
|
||||
|
||||
if (typeof query !== "object") {
|
||||
console.log("Invalid Query type");
|
||||
return str;
|
||||
}
|
||||
if (Array.isArray(query)) {
|
||||
console.log("Query is an Array. This is invalid.");
|
||||
return str;
|
||||
}
|
||||
if (!query) {
|
||||
console.log("No Query provided.");
|
||||
return str;
|
||||
}
|
||||
|
||||
const keys = Object.keys(query);
|
||||
|
||||
const queryArr: string[] = [];
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (!key || !query[key]) return;
|
||||
const value = query[key];
|
||||
|
||||
if (typeof value === "object") {
|
||||
const jsonStr = TWUIEJSON.stringify(value);
|
||||
queryArr.push(`${key}=${encodeURIComponent(String(jsonStr))}`);
|
||||
} else if (typeof value === "string" || typeof value === "number") {
|
||||
queryArr.push(`${key}=${encodeURIComponent(value)}`);
|
||||
} else {
|
||||
queryArr.push(`${key}=${String(value)}`);
|
||||
}
|
||||
});
|
||||
|
||||
str += queryArr.join("&");
|
||||
return str;
|
||||
}
|
||||
@ -31,7 +31,6 @@ export default function twuiSlugify(
|
||||
|
||||
return finalStr.replace(/-$/, "");
|
||||
} catch (error: any) {
|
||||
console.log(`Slugify ERROR: ${error.message}`);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import H2 from "@/components/lib/layout/H2";
|
||||
import Row from "@/components/lib/layout/Row";
|
||||
import Span from "@/components/lib/layout/Span";
|
||||
import Stack from "@/components/lib/layout/Stack";
|
||||
import { DSQL_TBENME_BLOG_POSTS } from "@/types";
|
||||
import { DSQL_TBEN_ME_BLOG_POSTS } from "@/types/dsql";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpRight,
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
post: DSQL_TBENME_BLOG_POSTS;
|
||||
post: DSQL_TBEN_ME_BLOG_POSTS;
|
||||
};
|
||||
|
||||
export default function BlogPostsListCard({ post }: Props) {
|
||||
|
||||
@ -6,11 +6,12 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"schema-to-typedef": "bunx dsql-schema-to-typedef",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@moduletrace/buncid": "^1.0.7",
|
||||
"@moduletrace/datasquirel": "^5.1.0",
|
||||
"@moduletrace/datasquirel": "^5.7.51",
|
||||
"@moduletrace/twui": "file:./components/lib",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-to-react": "^1.7.0",
|
||||
@ -18,6 +19,7 @@
|
||||
"lucide-react": "^0.462.0",
|
||||
"next": "15.0.3",
|
||||
"next-mdx-remote": "^5.0.0",
|
||||
"openai": "^6.21.0",
|
||||
"prism-themes": "^1.9.0",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
|
||||
@ -2,24 +2,30 @@ import Layout from "@/layouts/main";
|
||||
import Main from "@/components/pages/blog/slug";
|
||||
import { GetStaticPaths, GetStaticProps } from "next";
|
||||
import datasquirel from "@moduletrace/datasquirel";
|
||||
import { DSQL_TBENME_BLOG_POSTS, PagePropsType } from "@/types";
|
||||
import { PagePropsType } from "@/types";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import rehypePrismPlus from "rehype-prism-plus";
|
||||
import matter from "gray-matter";
|
||||
import { DSQL_TBEN_ME_BLOG_POSTS } from "@/types/dsql";
|
||||
|
||||
export default function SingleBlogPost() {
|
||||
export default function SingleBlogPost({ blogPost }: PagePropsType) {
|
||||
return (
|
||||
<Layout>
|
||||
<Layout
|
||||
meta={{
|
||||
title: blogPost?.meta_title,
|
||||
description: blogPost?.meta_description,
|
||||
}}
|
||||
>
|
||||
<Main />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<PagePropsType> = async (ctx) => {
|
||||
const blogPostRes: APIResponseObject<DSQL_TBENME_BLOG_POSTS[]> =
|
||||
await datasquirel.crud<DSQL_TBENME_BLOG_POSTS>({
|
||||
const blogPostRes: APIResponseObject<DSQL_TBEN_ME_BLOG_POSTS> =
|
||||
await datasquirel.crud<DSQL_TBEN_ME_BLOG_POSTS>({
|
||||
action: "get",
|
||||
table: "blog_posts",
|
||||
query: {
|
||||
@ -67,8 +73,8 @@ export const getStaticProps: GetStaticProps<PagePropsType> = async (ctx) => {
|
||||
};
|
||||
|
||||
export const getStaticPaths: GetStaticPaths = async (ctx) => {
|
||||
const blogPostRes: APIResponseObject<DSQL_TBENME_BLOG_POSTS[]> =
|
||||
await datasquirel.crud<DSQL_TBENME_BLOG_POSTS>({
|
||||
const blogPostRes: APIResponseObject<DSQL_TBEN_ME_BLOG_POSTS> =
|
||||
await datasquirel.crud<DSQL_TBEN_ME_BLOG_POSTS>({
|
||||
action: "get",
|
||||
table: "blog_posts",
|
||||
query: {
|
||||
|
||||
@ -2,8 +2,9 @@ import Layout from "@/layouts/main";
|
||||
import Main from "@/components/pages/blog";
|
||||
import { GetStaticProps } from "next";
|
||||
import datasquirel from "@moduletrace/datasquirel";
|
||||
import { DSQL_TBENME_BLOG_POSTS, PagePropsType } from "@/types";
|
||||
import { PagePropsType } from "@/types";
|
||||
import { APIResponseObject } from "@moduletrace/datasquirel/dist/package-shared/types";
|
||||
import { DSQL_TBEN_ME_BLOG_POSTS } from "@/types/dsql";
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
@ -14,8 +15,8 @@ export default function BlogPage() {
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps<PagePropsType> = async (ctx) => {
|
||||
const blogPosts: APIResponseObject<DSQL_TBENME_BLOG_POSTS[]> =
|
||||
await datasquirel.crud<DSQL_TBENME_BLOG_POSTS>({
|
||||
const blogPosts: APIResponseObject<DSQL_TBEN_ME_BLOG_POSTS> =
|
||||
await datasquirel.crud<DSQL_TBEN_ME_BLOG_POSTS>({
|
||||
action: "get",
|
||||
table: "blog_posts",
|
||||
query: {
|
||||
|
||||
21
types.ts
21
types.ts
@ -1,23 +1,8 @@
|
||||
import { MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
import { DSQL_TBEN_ME_BLOG_POSTS } from "./types/dsql";
|
||||
|
||||
export type PagePropsType = {
|
||||
blogPosts?: DSQL_TBENME_BLOG_POSTS[] | null;
|
||||
blogPost?: DSQL_TBENME_BLOG_POSTS | null;
|
||||
blogPosts?: DSQL_TBEN_ME_BLOG_POSTS[] | null;
|
||||
blogPost?: DSQL_TBEN_ME_BLOG_POSTS | null;
|
||||
mdxSource?: MDXRemoteSerializeResult<any, any> | null;
|
||||
};
|
||||
|
||||
export type DSQL_TBENME_BLOG_POSTS = {
|
||||
id?: number;
|
||||
title?: string;
|
||||
slug?: string;
|
||||
excerpt?: string;
|
||||
body?: string;
|
||||
metadata?: string;
|
||||
published?: 0 | 1;
|
||||
date_created?: string;
|
||||
date_created_code?: number;
|
||||
date_created_timestamp?: string;
|
||||
date_updated?: string;
|
||||
date_updated_code?: number;
|
||||
date_updated_timestamp?: string;
|
||||
};
|
||||
|
||||
55
types/dsql.ts
Normal file
55
types/dsql.ts
Normal file
@ -0,0 +1,55 @@
|
||||
export const DsqlTables = [
|
||||
"blog_posts",
|
||||
"portfolio",
|
||||
"documents",
|
||||
] as const
|
||||
|
||||
export type DSQL_TBEN_ME_BLOG_POSTS = {
|
||||
id?: number;
|
||||
title?: string;
|
||||
slug?: string;
|
||||
excerpt?: string;
|
||||
body?: string;
|
||||
metadata?: string;
|
||||
published?: 0 | 1;
|
||||
meta_title?: string;
|
||||
meta_description?: string;
|
||||
date_created?: string;
|
||||
date_created_code?: number;
|
||||
date_created_timestamp?: string;
|
||||
date_updated?: string;
|
||||
date_updated_code?: number;
|
||||
date_updated_timestamp?: string;
|
||||
}
|
||||
|
||||
export type DSQL_TBEN_ME_PORTFOLIO = {
|
||||
id?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
image?: string;
|
||||
full_description?: string;
|
||||
starting_date?: string;
|
||||
completion_date?: string;
|
||||
project_order?: number;
|
||||
date_created?: string;
|
||||
date_created_code?: number;
|
||||
date_created_timestamp?: string;
|
||||
date_updated?: string;
|
||||
date_updated_code?: number;
|
||||
date_updated_timestamp?: string;
|
||||
}
|
||||
|
||||
export type DSQL_TBEN_ME_DOCUMENTS = {
|
||||
id?: number;
|
||||
project_name?: string;
|
||||
html?: string;
|
||||
date_created?: string;
|
||||
date_created_code?: number;
|
||||
date_created_timestamp?: string;
|
||||
date_updated?: string;
|
||||
date_updated_code?: number;
|
||||
date_updated_timestamp?: string;
|
||||
}
|
||||
|
||||
export type DSQL_TBEN_ME_ALL_TYPEDEFS = DSQL_TBEN_ME_BLOG_POSTS & DSQL_TBEN_ME_PORTFOLIO & DSQL_TBEN_ME_DOCUMENTS
|
||||
Loading…
Reference in New Issue
Block a user