Updates
This commit is contained in:
parent
979728e6c8
commit
0b7c70058d
@ -69,7 +69,7 @@ export default function PopoverComponent({
|
||||
<Paper
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"max-w-[300px]",
|
||||
"max-w-[300px] z-[250]",
|
||||
"twui-popover-content",
|
||||
props.className
|
||||
)}
|
||||
|
||||
@ -60,6 +60,8 @@
|
||||
--radius-default-xs: 1px;
|
||||
--radius-default-lg: 7px;
|
||||
--radius-default-xl: 10px;
|
||||
|
||||
--container-container: 1200px;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
@ -155,3 +157,16 @@ option {
|
||||
.normal-text {
|
||||
@apply text-foreground-light dark:text-foreground-dark;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
@ -115,7 +115,7 @@ export default function TWUIDocsLink({
|
||||
<TWUIDocsLink
|
||||
key={index}
|
||||
docLink={link}
|
||||
className="text-sm opacity-70"
|
||||
className="opacity-70"
|
||||
autoExpandAll={autoExpandAll}
|
||||
child
|
||||
/>
|
||||
|
||||
@ -114,7 +114,7 @@ export default function AceEditor({
|
||||
return function () {
|
||||
editor.destroy();
|
||||
};
|
||||
}, [refresh, darkMode, ready, externalRefresh, content]);
|
||||
}, [refresh, darkMode, ready, externalRefresh]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const htmlClassName = document.documentElement.className;
|
||||
@ -145,7 +145,7 @@ export default function AceEditor({
|
||||
} catch (error: any) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span className="text-sm m-0">
|
||||
<span className="m-0">
|
||||
Editor Error:{" "}
|
||||
<b className="text-red-600">{error.message}</b>
|
||||
</span>
|
||||
|
||||
@ -3,9 +3,9 @@ import { RawEditorOptions, TinyMCE, Editor } from "./tinymce";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
|
||||
import Border from "../../elements/Border";
|
||||
import useTinyMCE from "./useTinyMCE";
|
||||
|
||||
export type TinyMCEEditorProps<KeyType extends string> = {
|
||||
tinyMCE?: TinyMCE | null;
|
||||
options?: RawEditorOptions;
|
||||
editorRef?: React.MutableRefObject<Editor | null>;
|
||||
setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
|
||||
@ -26,8 +26,6 @@ export type TinyMCEEditorProps<KeyType extends string> = {
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
let interval: any;
|
||||
|
||||
/**
|
||||
* # Tiny MCE Editor Component
|
||||
* @className_wrapper twui-rte-wrapper
|
||||
@ -35,8 +33,7 @@ let interval: any;
|
||||
export default function TinyMCEEditor<KeyType extends string>({
|
||||
options,
|
||||
editorRef,
|
||||
setEditor,
|
||||
tinyMCE,
|
||||
setEditor: passedSetEditor,
|
||||
wrapperProps,
|
||||
defaultValue,
|
||||
changeHandler,
|
||||
@ -47,29 +44,49 @@ export default function TinyMCEEditor<KeyType extends string>({
|
||||
useParentCSS,
|
||||
placeholder,
|
||||
}: TinyMCEEditorProps<KeyType>) {
|
||||
const { tinyMCE } = useTinyMCE();
|
||||
|
||||
const editorComponentRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const EDITOR_VALUE_CHANGE_TIMEOUT = 500;
|
||||
|
||||
const FINAL_HEIGHT = options?.height || 500;
|
||||
const [themeReady, setThemeReady] = React.useState(false);
|
||||
const [ready, setReady] = React.useState(false);
|
||||
const [darkMode, setDarkMode] = React.useState(false);
|
||||
const [refresh, setRefresh] = React.useState(0);
|
||||
const [editor, setEditor] = React.useState<Editor>();
|
||||
|
||||
const title = name ? twuiSlugToNormalText(name) : "Rich Text";
|
||||
|
||||
React.useEffect(() => {
|
||||
const htmlClassName = document.documentElement.className;
|
||||
if (htmlClassName.match(/dark/i)) setDarkMode(true);
|
||||
setTimeout(() => {
|
||||
setThemeReady(true);
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!editorComponentRef.current || !themeReady) {
|
||||
if (!tinyMCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
tinyMCE?.init({
|
||||
const htmlClassName = document.documentElement.className;
|
||||
|
||||
if (htmlClassName.match(/dark/i)) setDarkMode(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setThemeReady(true);
|
||||
}, 200);
|
||||
}, [tinyMCE]);
|
||||
|
||||
let valueTimeout: any;
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!editorComponentRef.current || !themeReady || !tinyMCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl =
|
||||
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
|
||||
"https://www.datasquirel.com/tinymce-public";
|
||||
|
||||
tinyMCE.init({
|
||||
height: FINAL_HEIGHT,
|
||||
menubar: false,
|
||||
plugins:
|
||||
@ -79,20 +96,30 @@ export default function TinyMCEEditor<KeyType extends string>({
|
||||
content_style:
|
||||
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }",
|
||||
init_instance_callback: (editor) => {
|
||||
setEditor?.(editor as any);
|
||||
setEditor(editor as any);
|
||||
if (editorRef) editorRef.current = editor as any;
|
||||
if (defaultValue) editor.setContent(defaultValue);
|
||||
setReady(true);
|
||||
|
||||
editor.on("change", (e) => {
|
||||
changeHandler?.(editor.getContent());
|
||||
// editor.on("change", (e) => {
|
||||
// changeHandler?.(editor.getContent());
|
||||
// });
|
||||
|
||||
editor.on("input", (e) => {
|
||||
if (changeHandler) {
|
||||
window.clearTimeout(valueTimeout);
|
||||
|
||||
valueTimeout = setTimeout(() => {
|
||||
changeHandler(editor.getContent());
|
||||
}, EDITOR_VALUE_CHANGE_TIMEOUT);
|
||||
}
|
||||
});
|
||||
|
||||
if (useParentCSS) {
|
||||
useParentStyles(editor);
|
||||
}
|
||||
},
|
||||
base_url: "https://www.datasquirel.com/tinymce-public",
|
||||
base_url: baseUrl,
|
||||
body_class: "twui-tinymce",
|
||||
placeholder,
|
||||
relative_urls: true,
|
||||
@ -106,9 +133,14 @@ export default function TinyMCEEditor<KeyType extends string>({
|
||||
});
|
||||
|
||||
return function () {
|
||||
tinyMCE?.remove();
|
||||
if (!ready) return;
|
||||
|
||||
const instance = editorComponentRef.current
|
||||
? tinyMCE?.get(editorComponentRef.current?.id)
|
||||
: undefined;
|
||||
instance?.remove();
|
||||
};
|
||||
}, [tinyMCE, themeReady]);
|
||||
}, [tinyMCE, themeReady, refresh]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -129,7 +161,7 @@ export default function TinyMCEEditor<KeyType extends string>({
|
||||
"bg-background-light dark:bg-background-dark text-gray-500",
|
||||
"dark:text-white/80 rounded"
|
||||
)}
|
||||
htmlFor={name || "twui-tinymce"}
|
||||
htmlFor={id}
|
||||
>
|
||||
{title}
|
||||
</label>
|
||||
@ -153,7 +185,7 @@ export default function TinyMCEEditor<KeyType extends string>({
|
||||
"bg-slate-200 dark:bg-slate-700 rounded-sm w-full",
|
||||
"twui-rte-wrapper"
|
||||
)}
|
||||
id={name || "twui-tinymce"}
|
||||
id={id}
|
||||
></div>
|
||||
</Border>
|
||||
</div>
|
||||
|
||||
@ -4,32 +4,49 @@ import { TinyMCE } from "./tinymce";
|
||||
let interval: any;
|
||||
|
||||
export default function useTinyMCE() {
|
||||
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE | null>(null);
|
||||
const [tinyMCE, setTinyMCE] = React.useState<TinyMCE>();
|
||||
const [refresh, setRefresh] = React.useState(0);
|
||||
const [scriptLoaded, setScriptLoaded] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
// @ts-ignore
|
||||
if (window.tinymce) {
|
||||
console.log("Tinymce already exists");
|
||||
// @ts-ignore
|
||||
setTinyMCE(window.tinymce);
|
||||
if (refresh >= 5) return;
|
||||
|
||||
const clientWindow = window as Window & { tinymce?: TinyMCE };
|
||||
|
||||
if (clientWindow.tinymce) {
|
||||
setScriptLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src =
|
||||
"https://www.datasquirel.com/tinymce-public/tinymce.min.js";
|
||||
|
||||
const baseUrl =
|
||||
process.env.NEXT_PUBLIC_TINYMCE_BASE_URL ||
|
||||
"https://www.datasquirel.com/tinymce-public";
|
||||
|
||||
script.src = `${baseUrl}/tinymce.min.js`;
|
||||
script.async = true;
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
script.onload = () => {
|
||||
// @ts-ignore
|
||||
if (window.tinymce) {
|
||||
// @ts-ignore
|
||||
setTinyMCE(window.tinymce);
|
||||
}
|
||||
setScriptLoaded(true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
document.head.appendChild(script);
|
||||
}, [refresh]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!scriptLoaded) return;
|
||||
|
||||
const clientWindow = window as Window & { tinymce?: TinyMCE };
|
||||
|
||||
let tinyMCE = clientWindow.tinymce;
|
||||
|
||||
if (tinyMCE) {
|
||||
setTinyMCE(tinyMCE);
|
||||
} else {
|
||||
setRefresh((prev) => prev + 1);
|
||||
}
|
||||
}, [scriptLoaded]);
|
||||
|
||||
return { tinyMCE };
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ export const TWUIDropdownContentPositions = [
|
||||
"left",
|
||||
"bottom-left",
|
||||
"top-left",
|
||||
"top",
|
||||
"bottom",
|
||||
"right",
|
||||
"bottom-right",
|
||||
"top-right",
|
||||
@ -160,6 +162,8 @@ export default function Dropdown({
|
||||
? "right-0 top-[100%]"
|
||||
: position == "center"
|
||||
? "left-[50%] -translate-x-[50%] top-[100%]"
|
||||
: position == "top"
|
||||
? "left-[50%] -translate-x-[50%] bottom-[100%]"
|
||||
: "top-[100%]",
|
||||
above ? "-translate-y-[120%]" : "",
|
||||
open ? "flex" : "hidden",
|
||||
|
||||
@ -2,19 +2,26 @@ import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Center from "../layout/Center";
|
||||
import Loading from "./Loading";
|
||||
import Row from "../layout/Row";
|
||||
import Span from "../layout/Span";
|
||||
|
||||
type Props = DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
loadingProps?: ComponentProps<typeof Loading>;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Loading Overlay Component
|
||||
* @className_wrapper twui-loading-overlay
|
||||
*/
|
||||
export default function LoadingOverlay({ loadingProps, ...props }: Props) {
|
||||
export default function LoadingOverlay({
|
||||
loadingProps,
|
||||
label,
|
||||
...props
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@ -26,7 +33,10 @@ export default function LoadingOverlay({ loadingProps, ...props }: Props) {
|
||||
)}
|
||||
>
|
||||
<Center>
|
||||
<Loading {...loadingProps} />
|
||||
<Row>
|
||||
<Loading {...loadingProps} />
|
||||
{label && <Span>{label}</Span>}
|
||||
</Row>
|
||||
</Center>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -32,6 +32,7 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
|
||||
trigger?: (typeof TWUIPopoverTriggers)[number];
|
||||
debounce?: number;
|
||||
onClose?: () => any;
|
||||
hoverOpen?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -55,6 +56,7 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
|
||||
trigger = "hover",
|
||||
debounce = 500,
|
||||
onClose,
|
||||
hoverOpen,
|
||||
} = props;
|
||||
|
||||
const [ready, setReady] = React.useState(false);
|
||||
@ -65,6 +67,9 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
|
||||
const modalRoot = document.getElementById(IDName);
|
||||
|
||||
if (modalRoot) {
|
||||
if (isPopover) {
|
||||
modalRoot.style.zIndex = "1000";
|
||||
}
|
||||
setReady(true);
|
||||
} else {
|
||||
const newModalRootEl = document.createElement("div");
|
||||
@ -144,12 +149,12 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
|
||||
onClick={(e) => setOpen(!open)}
|
||||
ref={finalTargetRef}
|
||||
onMouseEnter={
|
||||
isPopover && trigger === "hover"
|
||||
isPopover && (trigger === "hover" || hoverOpen)
|
||||
? popoverEnterFn
|
||||
: targetWrapperProps?.onMouseEnter
|
||||
}
|
||||
onMouseLeave={
|
||||
isPopover && trigger === "hover"
|
||||
isPopover && (trigger === "hover" || hoverOpen)
|
||||
? popoverLeaveFn
|
||||
: targetWrapperProps?.onMouseLeave
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ export type SearchProps<KeyType extends string> = DetailedHTMLProps<
|
||||
>;
|
||||
loading?: boolean;
|
||||
placeholder?: string;
|
||||
componentRef?: React.RefObject<HTMLInputElement | null>;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -37,6 +38,7 @@ export default function Search<KeyType extends string>({
|
||||
buttonProps,
|
||||
loading,
|
||||
placeholder,
|
||||
componentRef,
|
||||
...props
|
||||
}: SearchProps<KeyType>) {
|
||||
const [input, setInput] = React.useState(
|
||||
@ -52,7 +54,7 @@ export default function Search<KeyType extends string>({
|
||||
}, delay);
|
||||
}, [input]);
|
||||
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.autoFocus) {
|
||||
@ -64,7 +66,7 @@ export default function Search<KeyType extends string>({
|
||||
<Row
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"relative xl:flex-nowrap items-stretch gap-0",
|
||||
"relative xl:flex-nowrap items-stretch gap-0 flex-nowrap",
|
||||
"twui-search-wrapper",
|
||||
props?.className
|
||||
)}
|
||||
|
||||
@ -31,7 +31,7 @@ export default function Table({ data }: Props) {
|
||||
<th
|
||||
key={header}
|
||||
className={twMerge(
|
||||
"px-3 py-2 text-left text-sm opacity-50",
|
||||
"px-3 py-2 text-left opacity-50",
|
||||
"font-semibold"
|
||||
)}
|
||||
title={header}
|
||||
@ -58,7 +58,7 @@ export default function Table({ data }: Props) {
|
||||
<td
|
||||
key={`${header}-${index}`}
|
||||
className={twMerge(
|
||||
"px-3 py-2 whitespace-nowrap text-sm text-foreground-light",
|
||||
"px-3 py-2 whitespace-nowrap text-foreground-light",
|
||||
"dark:text-foreground-dark max-w-[200px] overflow-hidden",
|
||||
"overflow-ellipsis"
|
||||
)}
|
||||
|
||||
@ -8,7 +8,7 @@ import twuiSlugify from "../utils/slugify";
|
||||
export type TWUITabsObject = {
|
||||
title: string;
|
||||
value?: string;
|
||||
content: React.ReactNode;
|
||||
content?: React.ReactNode;
|
||||
defaultActive?: boolean;
|
||||
};
|
||||
|
||||
@ -35,6 +35,8 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
|
||||
* @className twui-tab-buttons
|
||||
* @className twui-tab-button-active
|
||||
* @className twui-tab-buttons-wrapper
|
||||
* @className twui-tab-buttons-container
|
||||
* @className twui-tabs-border
|
||||
*/
|
||||
export default function Tabs({
|
||||
tabsContentArray,
|
||||
@ -91,13 +93,14 @@ export default function Tabs({
|
||||
)}
|
||||
>
|
||||
<Border
|
||||
className="p-0 w-full overflow-hidden"
|
||||
className="p-0 w-full overflow-hidden twui-tabs-border"
|
||||
{...tabsBorderProps}
|
||||
>
|
||||
<Row
|
||||
className={twMerge(
|
||||
"gap-0 items-stretch w-full flex-nowrap overflow-x-auto",
|
||||
centered && "justify-center"
|
||||
centered && "justify-center",
|
||||
"twui-tab-buttons-container"
|
||||
)}
|
||||
>
|
||||
{values.map((value, index) => {
|
||||
|
||||
@ -18,6 +18,7 @@ type Props = {
|
||||
loading?: boolean;
|
||||
mdRes?: string;
|
||||
setMdRes: React.Dispatch<React.SetStateAction<string>>;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export default function AIPromptBlock({
|
||||
@ -27,6 +28,7 @@ export default function AIPromptBlock({
|
||||
loading = false,
|
||||
mdRes = "",
|
||||
setMdRes,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
const [prompt, setPrompt] = React.useState("");
|
||||
const currentPromptRef = React.useRef("");
|
||||
@ -43,7 +45,7 @@ export default function AIPromptBlock({
|
||||
<MessageCircleMore
|
||||
size={15}
|
||||
opacity={0.5}
|
||||
className="-mt-[1px]"
|
||||
className="-mt-px"
|
||||
/>
|
||||
</Row>
|
||||
</Card>
|
||||
@ -53,11 +55,15 @@ export default function AIPromptBlock({
|
||||
setStreamRes={setMdRes}
|
||||
streamRes={mdRes}
|
||||
history={history}
|
||||
loading={loading}
|
||||
/>
|
||||
<Stack className="w-full relative">
|
||||
{loading && <LoadingOverlay />}
|
||||
<Textarea
|
||||
placeholder={model ? `Prompt ${model}` : "Prompt AI"}
|
||||
placeholder={
|
||||
placeholder ||
|
||||
(model ? `Prompt ${model}` : "Prompt AI")
|
||||
}
|
||||
wrapperProps={{ className: "outline-none" }}
|
||||
wrapperWrapperProps={{ className: "w-full" }}
|
||||
value={prompt}
|
||||
@ -65,7 +71,7 @@ export default function AIPromptBlock({
|
||||
setPrompt(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key == "Enter" && !e.ctrlKey) {
|
||||
if (e.key == "Enter" && !e.ctrlKey && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
currentPromptRef.current = prompt;
|
||||
setTimeout(() => {
|
||||
|
||||
@ -8,12 +8,14 @@ type Props = {
|
||||
streamRes: string;
|
||||
setStreamRes: React.Dispatch<React.SetStateAction<string>>;
|
||||
history: ChatCompletionMessageParam[];
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
export default function AIPromptPreview({
|
||||
setStreamRes,
|
||||
streamRes,
|
||||
history,
|
||||
loading,
|
||||
}: Props) {
|
||||
const responseContentRef = React.useRef<HTMLDivElement>(null);
|
||||
const isContentInterrupted = React.useRef(false);
|
||||
@ -27,7 +29,7 @@ export default function AIPromptPreview({
|
||||
}
|
||||
}, [streamRes]);
|
||||
|
||||
if (!streamRes?.match(/./)) return null;
|
||||
if (loading || !streamRes?.match(/./)) return null;
|
||||
|
||||
return (
|
||||
<Stack className="w-full">
|
||||
|
||||
@ -11,6 +11,7 @@ import fileInputToBase64 from "../utils/form/fileInputToBase64";
|
||||
import Row from "../layout/Row";
|
||||
import Input from "./Input";
|
||||
import Loading from "../elements/Loading";
|
||||
import Tag from "../elements/Tag";
|
||||
|
||||
type FileInputUtils = {
|
||||
clearFileInput?: () => void;
|
||||
@ -59,6 +60,7 @@ type ImageUploadProps = DetailedHTMLProps<
|
||||
existingFile?: FileInputToBase64FunctionReturn;
|
||||
existingFileUrl?: string;
|
||||
icon?: ReactNode;
|
||||
externalSetFileURL?: React.Dispatch<string | undefined>;
|
||||
labelSpanProps?: ComponentProps<typeof Span>;
|
||||
loading?: boolean;
|
||||
multiple?: boolean;
|
||||
@ -86,6 +88,7 @@ export default function FileUpload({
|
||||
multiple,
|
||||
onClear,
|
||||
changeHandler,
|
||||
externalSetFileURL,
|
||||
...props
|
||||
}: ImageUploadProps) {
|
||||
const [file, setFile] = React.useState<
|
||||
@ -97,6 +100,7 @@ export default function FileUpload({
|
||||
|
||||
const [fileDraggedOver, setFileDraggedOver] = React.useState(false);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const tempFileURLRef = React.useRef<string>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (existingFileUrl) {
|
||||
@ -244,26 +248,37 @@ export default function FileUpload({
|
||||
className="w-full relative h-full items-center justify-center overflow-hidden"
|
||||
{...previewImageWrapperProps}
|
||||
>
|
||||
{disablePreview ? (
|
||||
<Span className="opacity-50" size="small">
|
||||
Image Uploaded!
|
||||
</Span>
|
||||
) : fileUrl.match(/\.pdf$|\.txt$/) ? (
|
||||
<Row>
|
||||
<FileArchive size={36} strokeWidth={1} />
|
||||
<Stack className="gap-0">
|
||||
<Span size="smaller" className="opacity-70">
|
||||
{fileUrl}
|
||||
</Span>
|
||||
</Stack>
|
||||
</Row>
|
||||
) : (
|
||||
<img
|
||||
src={fileUrl}
|
||||
className="w-full object-contain overflow-hidden"
|
||||
{...previewImageProps}
|
||||
/>
|
||||
)}
|
||||
<Stack className="w-full">
|
||||
{disablePreview ? (
|
||||
<Span className="opacity-50" size="small">
|
||||
Image Uploaded!
|
||||
</Span>
|
||||
) : fileUrl.match(/\.pdf$|\.txt$/) ? (
|
||||
<Row>
|
||||
<FileArchive size={36} strokeWidth={1} />
|
||||
<Stack className="gap-0">
|
||||
<Span size="smaller" className="opacity-70">
|
||||
{fileUrl}
|
||||
</Span>
|
||||
</Stack>
|
||||
</Row>
|
||||
) : (
|
||||
<img
|
||||
src={fileUrl}
|
||||
className="w-full object-contain overflow-hidden"
|
||||
{...previewImageProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Tag
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className="w-full py-2 text-sm"
|
||||
>
|
||||
{fileUrl}
|
||||
</Tag>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={twMerge(
|
||||
@ -345,6 +360,59 @@ export default function FileUpload({
|
||||
>
|
||||
{label || "Click to Upload File"}
|
||||
</Span>
|
||||
{externalSetFileURL ? (
|
||||
<Row
|
||||
className="flex-nowrap gap-0 items-stretch"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add Media URL"
|
||||
className="text-sm"
|
||||
wrapperProps={{ className: "h-full" }}
|
||||
wrapperWrapperProps={{
|
||||
className: "h-full",
|
||||
}}
|
||||
changeHandler={(value) => {
|
||||
tempFileURLRef.current = value;
|
||||
}}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key == "Enter") {
|
||||
if (tempFileURLRef.current) {
|
||||
setFileUrl(
|
||||
tempFileURLRef.current
|
||||
);
|
||||
externalSetFileURL(
|
||||
tempFileURLRef.current
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Add Media URL"
|
||||
variant="outlined"
|
||||
className="py-0 px-3"
|
||||
color="gray"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (tempFileURLRef.current) {
|
||||
setFileUrl(
|
||||
tempFileURLRef.current
|
||||
);
|
||||
externalSetFileURL(
|
||||
tempFileURLRef.current
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-2xl">+</span>
|
||||
</Button>
|
||||
</Row>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Center>
|
||||
</Card>
|
||||
|
||||
@ -207,14 +207,16 @@ export default function Input<KeyType extends string>(
|
||||
|
||||
window.clearTimeout(timeout);
|
||||
|
||||
if (validationRegex && !validationFunction) {
|
||||
if (validationRegex) {
|
||||
timeout = setTimeout(() => {
|
||||
setValidity({
|
||||
isValid: validationRegex.test(val),
|
||||
msg: "Value mismatch",
|
||||
});
|
||||
}, finalDebounce);
|
||||
} else if (validationFunction) {
|
||||
}
|
||||
|
||||
if (validationFunction) {
|
||||
window.clearTimeout(validationFnTimeout);
|
||||
|
||||
validationFnTimeout = setTimeout(() => {
|
||||
|
||||
@ -232,9 +232,9 @@ export default function Select<
|
||||
}
|
||||
hoverOpen
|
||||
>
|
||||
<Card className="min-w-[250px] text-sm p-6">
|
||||
<Card className="min-w-[250px] p-6">
|
||||
{typeof info == "string" ? (
|
||||
<Span className="text-sm">{info}</Span>
|
||||
<Span>{info}</Span>
|
||||
) : (
|
||||
info
|
||||
)}
|
||||
|
||||
@ -258,7 +258,7 @@ export default function Button({
|
||||
props.disabled ? "opacity-40 cursor-not-allowed" : "",
|
||||
"twui-button-general",
|
||||
size == "small"
|
||||
? "px-3 py-1.5 text-sm twui-button-small"
|
||||
? "px-3 py-1.5 twui-button-small text-sm"
|
||||
: size == "smaller"
|
||||
? "px-2 py-1 text-xs twui-button-smaller"
|
||||
: size == "large"
|
||||
|
||||
@ -12,7 +12,7 @@ export default function Container({
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex w-full max-w-[1200px] gap-4 justify-between",
|
||||
"flex w-full max-w-container gap-4 justify-between",
|
||||
"flex-wrap flex-col xl:flex-row items-start xl:items-center",
|
||||
"twui-container",
|
||||
props.className
|
||||
|
||||
@ -12,7 +12,7 @@ export default function H5({
|
||||
<h5
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"text-sm mb-4",
|
||||
"mb-4",
|
||||
"twui-headings twui-heading",
|
||||
"twui-h5",
|
||||
props.className
|
||||
|
||||
@ -18,7 +18,7 @@ export default function Spacer({ horizontal, ...props }: Props) {
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"grow",
|
||||
"",
|
||||
horizontal ? "w-10" : "w-full h-10",
|
||||
"twui-spacer",
|
||||
props.className
|
||||
|
||||
@ -23,6 +23,7 @@ export function useMDXComponents(params?: Params) {
|
||||
pre: ({ children, ...props }) => {
|
||||
if (React.isValidElement(children) && children.props) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
||||
{/* @ts-ignore */}
|
||||
{children.props.children}
|
||||
@ -30,6 +31,7 @@ export function useMDXComponents(params?: Params) {
|
||||
);
|
||||
}
|
||||
return (
|
||||
// @ts-ignore
|
||||
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
||||
{children}
|
||||
</CodeBlock>
|
||||
@ -62,6 +64,7 @@ export function useMDXComponents(params?: Params) {
|
||||
);
|
||||
},
|
||||
img: (props) => (
|
||||
// @ts-ignore
|
||||
<img
|
||||
{...props}
|
||||
className="w-full h-auto shadow-lg rounded-default overflow-hidden"
|
||||
|
||||
@ -22,6 +22,7 @@ export default function useMDXComponents({
|
||||
pre: ({ children, ...props }) => {
|
||||
if (React.isValidElement(children) && children.props) {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
||||
{/* @ts-ignore */}
|
||||
{children.props.children}
|
||||
@ -29,6 +30,7 @@ export default function useMDXComponents({
|
||||
);
|
||||
}
|
||||
return (
|
||||
// @ts-ignore
|
||||
<CodeBlock {...props} backgroundColor={codeBgColor}>
|
||||
{children}
|
||||
</CodeBlock>
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import _ from "lodash";
|
||||
|
||||
export const FetchAPIMethods = [
|
||||
"POST",
|
||||
"GET",
|
||||
"DELETE",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"post",
|
||||
"get",
|
||||
"delete",
|
||||
"put",
|
||||
"patch",
|
||||
] as const;
|
||||
|
||||
type FetchApiOptions<T extends { [k: string]: any } = { [k: string]: any }> = {
|
||||
method:
|
||||
| "POST"
|
||||
| "GET"
|
||||
| "DELETE"
|
||||
| "PUT"
|
||||
| "PATCH"
|
||||
| "post"
|
||||
| "get"
|
||||
| "delete"
|
||||
| "put"
|
||||
| "patch";
|
||||
method: (typeof FetchAPIMethods)[number];
|
||||
body?: T | string;
|
||||
headers?: FetchHeader;
|
||||
};
|
||||
|
||||
@ -35,7 +35,7 @@ export default async function fileInputToBase64({
|
||||
resolve(reader.result?.toString());
|
||||
};
|
||||
reader.onerror = function (/** @type {*} */ error: any) {
|
||||
console.log("Error: ", error.message);
|
||||
console.log("File Input to Base64 Error: ", error.message);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@ -4,14 +4,28 @@ import Header from "./Header";
|
||||
import Footer from "./Footer";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import MobileMenu from "./(sections)/MobileMenu";
|
||||
import Head from "next/head";
|
||||
|
||||
type Props = PropsWithChildren & {};
|
||||
type Props = PropsWithChildren & {
|
||||
meta?: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Layout({ children }: Props) {
|
||||
export default function Layout({ children, meta }: Props) {
|
||||
const [menuOpen, setMenuOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-stretch w-full min-h-screen">
|
||||
<Head>
|
||||
<title>
|
||||
{meta?.title || "10X Software/Devops Engineer | Tben.me"}
|
||||
</title>
|
||||
{meta?.description && (
|
||||
<meta name="description" content={meta?.description} />
|
||||
)}
|
||||
</Head>
|
||||
<Aside />
|
||||
<div className={twMerge("flex flex-col items-start gap-0", "grow")}>
|
||||
<Header {...{ menuOpen, setMenuOpen }} />
|
||||
|
||||
@ -3,7 +3,7 @@ import Main from "@/components/pages/about";
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<Layout meta={{ title: "About Me | Tben.me" }}>
|
||||
<Main />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@ -3,7 +3,7 @@ import Main from "@/components/pages/contact";
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<Layout meta={{ title: "Contact Me | Tben.me" }}>
|
||||
<Main />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import Layout from "@/layouts/main";
|
||||
import H1 from "@/components/lib/layout/H1";
|
||||
import Main from "@/components/pages/Home";
|
||||
import AboutSection from "@/components/pages/Home/(sections)/AboutSection";
|
||||
import Divider from "@/components/lib/layout/Divider";
|
||||
@ -8,7 +7,12 @@ import MyWorkSection from "@/components/pages/Home/(sections)/MyWorkSection";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<Layout>
|
||||
<Layout
|
||||
meta={{
|
||||
description:
|
||||
"Software Engineer, DevOps Engineer, Full Stack Developer, Software Architect, Philosopher, Solar Energy Enthusiast.",
|
||||
}}
|
||||
>
|
||||
<Main />
|
||||
<Divider />
|
||||
<AboutSection />
|
||||
|
||||
@ -5,7 +5,7 @@ import MySkillsSection from "@/components/pages/Home/(sections)/MySkillsSection"
|
||||
|
||||
export default function SkillsPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<Layout meta={{ title: "My Skills | Tben.me" }}>
|
||||
<Main />
|
||||
<Divider />
|
||||
<MySkillsSection noTitle expand />
|
||||
|
||||
@ -5,7 +5,7 @@ import MyWorkSection from "@/components/pages/Home/(sections)/MyWorkSection";
|
||||
|
||||
export default function WorkPage() {
|
||||
return (
|
||||
<Layout>
|
||||
<Layout meta={{ title: "My Work | Tben.me" }}>
|
||||
<Main />
|
||||
<Divider />
|
||||
<MyWorkSection noTitle expand />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user