This commit is contained in:
Benjamin Toby 2025-12-02 16:30:46 +01:00
parent 979728e6c8
commit 0b7c70058d
31 changed files with 298 additions and 106 deletions

View File

@ -69,7 +69,7 @@ export default function PopoverComponent({
<Paper <Paper
{...props} {...props}
className={twMerge( className={twMerge(
"max-w-[300px]", "max-w-[300px] z-[250]",
"twui-popover-content", "twui-popover-content",
props.className props.className
)} )}

View File

@ -60,6 +60,8 @@
--radius-default-xs: 1px; --radius-default-xs: 1px;
--radius-default-lg: 7px; --radius-default-lg: 7px;
--radius-default-xl: 10px; --radius-default-xl: 10px;
--container-container: 1200px;
} }
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@ -155,3 +157,16 @@ option {
.normal-text { .normal-text {
@apply text-foreground-light dark:text-foreground-dark; @apply text-foreground-light dark:text-foreground-dark;
} }
ol {
list-style: decimal;
}
ul {
list-style: disc;
}
ul,
ol {
margin-left: 25px;
}

View File

@ -115,7 +115,7 @@ export default function TWUIDocsLink({
<TWUIDocsLink <TWUIDocsLink
key={index} key={index}
docLink={link} docLink={link}
className="text-sm opacity-70" className="opacity-70"
autoExpandAll={autoExpandAll} autoExpandAll={autoExpandAll}
child child
/> />

View File

@ -114,7 +114,7 @@ export default function AceEditor({
return function () { return function () {
editor.destroy(); editor.destroy();
}; };
}, [refresh, darkMode, ready, externalRefresh, content]); }, [refresh, darkMode, ready, externalRefresh]);
React.useEffect(() => { React.useEffect(() => {
const htmlClassName = document.documentElement.className; const htmlClassName = document.documentElement.className;
@ -145,7 +145,7 @@ export default function AceEditor({
} catch (error: any) { } catch (error: any) {
return ( return (
<React.Fragment> <React.Fragment>
<span className="text-sm m-0"> <span className="m-0">
Editor Error:{" "} Editor Error:{" "}
<b className="text-red-600">{error.message}</b> <b className="text-red-600">{error.message}</b>
</span> </span>

View File

@ -3,9 +3,9 @@ import { RawEditorOptions, TinyMCE, Editor } from "./tinymce";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import twuiSlugToNormalText from "../../utils/slug-to-normal-text"; import twuiSlugToNormalText from "../../utils/slug-to-normal-text";
import Border from "../../elements/Border"; import Border from "../../elements/Border";
import useTinyMCE from "./useTinyMCE";
export type TinyMCEEditorProps<KeyType extends string> = { export type TinyMCEEditorProps<KeyType extends string> = {
tinyMCE?: TinyMCE | null;
options?: RawEditorOptions; options?: RawEditorOptions;
editorRef?: React.MutableRefObject<Editor | null>; editorRef?: React.MutableRefObject<Editor | null>;
setEditor?: React.Dispatch<React.SetStateAction<Editor>>; setEditor?: React.Dispatch<React.SetStateAction<Editor>>;
@ -26,8 +26,6 @@ export type TinyMCEEditorProps<KeyType extends string> = {
placeholder?: string; placeholder?: string;
}; };
let interval: any;
/** /**
* # Tiny MCE Editor Component * # Tiny MCE Editor Component
* @className_wrapper twui-rte-wrapper * @className_wrapper twui-rte-wrapper
@ -35,8 +33,7 @@ let interval: any;
export default function TinyMCEEditor<KeyType extends string>({ export default function TinyMCEEditor<KeyType extends string>({
options, options,
editorRef, editorRef,
setEditor, setEditor: passedSetEditor,
tinyMCE,
wrapperProps, wrapperProps,
defaultValue, defaultValue,
changeHandler, changeHandler,
@ -47,29 +44,49 @@ export default function TinyMCEEditor<KeyType extends string>({
useParentCSS, useParentCSS,
placeholder, placeholder,
}: TinyMCEEditorProps<KeyType>) { }: TinyMCEEditorProps<KeyType>) {
const { tinyMCE } = useTinyMCE();
const editorComponentRef = React.useRef<HTMLDivElement>(null); const editorComponentRef = React.useRef<HTMLDivElement>(null);
const EDITOR_VALUE_CHANGE_TIMEOUT = 500;
const FINAL_HEIGHT = options?.height || 500; const FINAL_HEIGHT = options?.height || 500;
const [themeReady, setThemeReady] = React.useState(false); const [themeReady, setThemeReady] = React.useState(false);
const [ready, setReady] = React.useState(false); const [ready, setReady] = React.useState(false);
const [darkMode, setDarkMode] = 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"; const title = name ? twuiSlugToNormalText(name) : "Rich Text";
React.useEffect(() => { React.useEffect(() => {
const htmlClassName = document.documentElement.className; if (!tinyMCE) {
if (htmlClassName.match(/dark/i)) setDarkMode(true);
setTimeout(() => {
setThemeReady(true);
}, 200);
}, []);
React.useEffect(() => {
if (!editorComponentRef.current || !themeReady) {
return; 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, height: FINAL_HEIGHT,
menubar: false, menubar: false,
plugins: plugins:
@ -79,20 +96,30 @@ export default function TinyMCEEditor<KeyType extends string>({
content_style: content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }", "body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }",
init_instance_callback: (editor) => { init_instance_callback: (editor) => {
setEditor?.(editor as any); setEditor(editor as any);
if (editorRef) editorRef.current = editor as any; if (editorRef) editorRef.current = editor as any;
if (defaultValue) editor.setContent(defaultValue); if (defaultValue) editor.setContent(defaultValue);
setReady(true); setReady(true);
editor.on("change", (e) => { // editor.on("change", (e) => {
changeHandler?.(editor.getContent()); // changeHandler?.(editor.getContent());
// });
editor.on("input", (e) => {
if (changeHandler) {
window.clearTimeout(valueTimeout);
valueTimeout = setTimeout(() => {
changeHandler(editor.getContent());
}, EDITOR_VALUE_CHANGE_TIMEOUT);
}
}); });
if (useParentCSS) { if (useParentCSS) {
useParentStyles(editor); useParentStyles(editor);
} }
}, },
base_url: "https://www.datasquirel.com/tinymce-public", base_url: baseUrl,
body_class: "twui-tinymce", body_class: "twui-tinymce",
placeholder, placeholder,
relative_urls: true, relative_urls: true,
@ -106,9 +133,14 @@ export default function TinyMCEEditor<KeyType extends string>({
}); });
return function () { 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 ( return (
<div <div
@ -129,7 +161,7 @@ export default function TinyMCEEditor<KeyType extends string>({
"bg-background-light dark:bg-background-dark text-gray-500", "bg-background-light dark:bg-background-dark text-gray-500",
"dark:text-white/80 rounded" "dark:text-white/80 rounded"
)} )}
htmlFor={name || "twui-tinymce"} htmlFor={id}
> >
{title} {title}
</label> </label>
@ -153,7 +185,7 @@ export default function TinyMCEEditor<KeyType extends string>({
"bg-slate-200 dark:bg-slate-700 rounded-sm w-full", "bg-slate-200 dark:bg-slate-700 rounded-sm w-full",
"twui-rte-wrapper" "twui-rte-wrapper"
)} )}
id={name || "twui-tinymce"} id={id}
></div> ></div>
</Border> </Border>
</div> </div>

View File

@ -4,32 +4,49 @@ import { TinyMCE } from "./tinymce";
let interval: any; let interval: any;
export default function useTinyMCE() { 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(() => { React.useEffect(() => {
// @ts-ignore if (refresh >= 5) return;
if (window.tinymce) {
console.log("Tinymce already exists"); const clientWindow = window as Window & { tinymce?: TinyMCE };
// @ts-ignore
setTinyMCE(window.tinymce); if (clientWindow.tinymce) {
setScriptLoaded(true);
return; return;
} }
const script = document.createElement("script"); 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; script.async = true;
document.head.appendChild(script);
script.onload = () => { script.onload = () => {
// @ts-ignore setScriptLoaded(true);
if (window.tinymce) {
// @ts-ignore
setTinyMCE(window.tinymce);
}
}; };
}, []);
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 }; return { tinyMCE };
} }

View File

@ -9,6 +9,8 @@ export const TWUIDropdownContentPositions = [
"left", "left",
"bottom-left", "bottom-left",
"top-left", "top-left",
"top",
"bottom",
"right", "right",
"bottom-right", "bottom-right",
"top-right", "top-right",
@ -160,6 +162,8 @@ export default function Dropdown({
? "right-0 top-[100%]" ? "right-0 top-[100%]"
: position == "center" : position == "center"
? "left-[50%] -translate-x-[50%] top-[100%]" ? "left-[50%] -translate-x-[50%] top-[100%]"
: position == "top"
? "left-[50%] -translate-x-[50%] bottom-[100%]"
: "top-[100%]", : "top-[100%]",
above ? "-translate-y-[120%]" : "", above ? "-translate-y-[120%]" : "",
open ? "flex" : "hidden", open ? "flex" : "hidden",

View File

@ -2,19 +2,26 @@ import { ComponentProps, DetailedHTMLProps, HTMLAttributes } from "react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import Center from "../layout/Center"; import Center from "../layout/Center";
import Loading from "./Loading"; import Loading from "./Loading";
import Row from "../layout/Row";
import Span from "../layout/Span";
type Props = DetailedHTMLProps< type Props = DetailedHTMLProps<
HTMLAttributes<HTMLDivElement>, HTMLAttributes<HTMLDivElement>,
HTMLDivElement HTMLDivElement
> & { > & {
loadingProps?: ComponentProps<typeof Loading>; loadingProps?: ComponentProps<typeof Loading>;
label?: string;
}; };
/** /**
* # Loading Overlay Component * # Loading Overlay Component
* @className_wrapper twui-loading-overlay * @className_wrapper twui-loading-overlay
*/ */
export default function LoadingOverlay({ loadingProps, ...props }: Props) { export default function LoadingOverlay({
loadingProps,
label,
...props
}: Props) {
return ( return (
<div <div
{...props} {...props}
@ -26,7 +33,10 @@ export default function LoadingOverlay({ loadingProps, ...props }: Props) {
)} )}
> >
<Center> <Center>
<Row>
<Loading {...loadingProps} /> <Loading {...loadingProps} />
{label && <Span>{label}</Span>}
</Row>
</Center> </Center>
</div> </div>
); );

View File

@ -32,6 +32,7 @@ export type TWUI_MODAL_PROPS = DetailedHTMLProps<
trigger?: (typeof TWUIPopoverTriggers)[number]; trigger?: (typeof TWUIPopoverTriggers)[number];
debounce?: number; debounce?: number;
onClose?: () => any; onClose?: () => any;
hoverOpen?: boolean;
}; };
/** /**
@ -55,6 +56,7 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
trigger = "hover", trigger = "hover",
debounce = 500, debounce = 500,
onClose, onClose,
hoverOpen,
} = props; } = props;
const [ready, setReady] = React.useState(false); const [ready, setReady] = React.useState(false);
@ -65,6 +67,9 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
const modalRoot = document.getElementById(IDName); const modalRoot = document.getElementById(IDName);
if (modalRoot) { if (modalRoot) {
if (isPopover) {
modalRoot.style.zIndex = "1000";
}
setReady(true); setReady(true);
} else { } else {
const newModalRootEl = document.createElement("div"); const newModalRootEl = document.createElement("div");
@ -144,12 +149,12 @@ export default function Modal(props: TWUI_MODAL_PROPS) {
onClick={(e) => setOpen(!open)} onClick={(e) => setOpen(!open)}
ref={finalTargetRef} ref={finalTargetRef}
onMouseEnter={ onMouseEnter={
isPopover && trigger === "hover" isPopover && (trigger === "hover" || hoverOpen)
? popoverEnterFn ? popoverEnterFn
: targetWrapperProps?.onMouseEnter : targetWrapperProps?.onMouseEnter
} }
onMouseLeave={ onMouseLeave={
isPopover && trigger === "hover" isPopover && (trigger === "hover" || hoverOpen)
? popoverLeaveFn ? popoverLeaveFn
: targetWrapperProps?.onMouseLeave : targetWrapperProps?.onMouseLeave
} }

View File

@ -21,6 +21,7 @@ export type SearchProps<KeyType extends string> = DetailedHTMLProps<
>; >;
loading?: boolean; loading?: boolean;
placeholder?: string; placeholder?: string;
componentRef?: React.RefObject<HTMLInputElement | null>;
}; };
/** /**
@ -37,6 +38,7 @@ export default function Search<KeyType extends string>({
buttonProps, buttonProps,
loading, loading,
placeholder, placeholder,
componentRef,
...props ...props
}: SearchProps<KeyType>) { }: SearchProps<KeyType>) {
const [input, setInput] = React.useState( const [input, setInput] = React.useState(
@ -52,7 +54,7 @@ export default function Search<KeyType extends string>({
}, delay); }, delay);
}, [input]); }, [input]);
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = componentRef || React.useRef<HTMLInputElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (props.autoFocus) { if (props.autoFocus) {
@ -64,7 +66,7 @@ export default function Search<KeyType extends string>({
<Row <Row
{...props} {...props}
className={twMerge( className={twMerge(
"relative xl:flex-nowrap items-stretch gap-0", "relative xl:flex-nowrap items-stretch gap-0 flex-nowrap",
"twui-search-wrapper", "twui-search-wrapper",
props?.className props?.className
)} )}

View File

@ -31,7 +31,7 @@ export default function Table({ data }: Props) {
<th <th
key={header} key={header}
className={twMerge( className={twMerge(
"px-3 py-2 text-left text-sm opacity-50", "px-3 py-2 text-left opacity-50",
"font-semibold" "font-semibold"
)} )}
title={header} title={header}
@ -58,7 +58,7 @@ export default function Table({ data }: Props) {
<td <td
key={`${header}-${index}`} key={`${header}-${index}`}
className={twMerge( 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", "dark:text-foreground-dark max-w-[200px] overflow-hidden",
"overflow-ellipsis" "overflow-ellipsis"
)} )}

View File

@ -8,7 +8,7 @@ import twuiSlugify from "../utils/slugify";
export type TWUITabsObject = { export type TWUITabsObject = {
title: string; title: string;
value?: string; value?: string;
content: React.ReactNode; content?: React.ReactNode;
defaultActive?: boolean; defaultActive?: boolean;
}; };
@ -35,6 +35,8 @@ export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
* @className twui-tab-buttons * @className twui-tab-buttons
* @className twui-tab-button-active * @className twui-tab-button-active
* @className twui-tab-buttons-wrapper * @className twui-tab-buttons-wrapper
* @className twui-tab-buttons-container
* @className twui-tabs-border
*/ */
export default function Tabs({ export default function Tabs({
tabsContentArray, tabsContentArray,
@ -91,13 +93,14 @@ export default function Tabs({
)} )}
> >
<Border <Border
className="p-0 w-full overflow-hidden" className="p-0 w-full overflow-hidden twui-tabs-border"
{...tabsBorderProps} {...tabsBorderProps}
> >
<Row <Row
className={twMerge( className={twMerge(
"gap-0 items-stretch w-full flex-nowrap overflow-x-auto", "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) => { {values.map((value, index) => {

View File

@ -18,6 +18,7 @@ type Props = {
loading?: boolean; loading?: boolean;
mdRes?: string; mdRes?: string;
setMdRes: React.Dispatch<React.SetStateAction<string>>; setMdRes: React.Dispatch<React.SetStateAction<string>>;
placeholder?: string;
}; };
export default function AIPromptBlock({ export default function AIPromptBlock({
@ -27,6 +28,7 @@ export default function AIPromptBlock({
loading = false, loading = false,
mdRes = "", mdRes = "",
setMdRes, setMdRes,
placeholder,
}: Props) { }: Props) {
const [prompt, setPrompt] = React.useState(""); const [prompt, setPrompt] = React.useState("");
const currentPromptRef = React.useRef(""); const currentPromptRef = React.useRef("");
@ -43,7 +45,7 @@ export default function AIPromptBlock({
<MessageCircleMore <MessageCircleMore
size={15} size={15}
opacity={0.5} opacity={0.5}
className="-mt-[1px]" className="-mt-px"
/> />
</Row> </Row>
</Card> </Card>
@ -53,11 +55,15 @@ export default function AIPromptBlock({
setStreamRes={setMdRes} setStreamRes={setMdRes}
streamRes={mdRes} streamRes={mdRes}
history={history} history={history}
loading={loading}
/> />
<Stack className="w-full relative"> <Stack className="w-full relative">
{loading && <LoadingOverlay />} {loading && <LoadingOverlay />}
<Textarea <Textarea
placeholder={model ? `Prompt ${model}` : "Prompt AI"} placeholder={
placeholder ||
(model ? `Prompt ${model}` : "Prompt AI")
}
wrapperProps={{ className: "outline-none" }} wrapperProps={{ className: "outline-none" }}
wrapperWrapperProps={{ className: "w-full" }} wrapperWrapperProps={{ className: "w-full" }}
value={prompt} value={prompt}
@ -65,7 +71,7 @@ export default function AIPromptBlock({
setPrompt(e.target.value); setPrompt(e.target.value);
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key == "Enter" && !e.ctrlKey) { if (e.key == "Enter" && !e.ctrlKey && !e.shiftKey) {
e.preventDefault(); e.preventDefault();
currentPromptRef.current = prompt; currentPromptRef.current = prompt;
setTimeout(() => { setTimeout(() => {

View File

@ -8,12 +8,14 @@ type Props = {
streamRes: string; streamRes: string;
setStreamRes: React.Dispatch<React.SetStateAction<string>>; setStreamRes: React.Dispatch<React.SetStateAction<string>>;
history: ChatCompletionMessageParam[]; history: ChatCompletionMessageParam[];
loading?: boolean;
}; };
export default function AIPromptPreview({ export default function AIPromptPreview({
setStreamRes, setStreamRes,
streamRes, streamRes,
history, history,
loading,
}: Props) { }: Props) {
const responseContentRef = React.useRef<HTMLDivElement>(null); const responseContentRef = React.useRef<HTMLDivElement>(null);
const isContentInterrupted = React.useRef(false); const isContentInterrupted = React.useRef(false);
@ -27,7 +29,7 @@ export default function AIPromptPreview({
} }
}, [streamRes]); }, [streamRes]);
if (!streamRes?.match(/./)) return null; if (loading || !streamRes?.match(/./)) return null;
return ( return (
<Stack className="w-full"> <Stack className="w-full">

View File

@ -11,6 +11,7 @@ import fileInputToBase64 from "../utils/form/fileInputToBase64";
import Row from "../layout/Row"; import Row from "../layout/Row";
import Input from "./Input"; import Input from "./Input";
import Loading from "../elements/Loading"; import Loading from "../elements/Loading";
import Tag from "../elements/Tag";
type FileInputUtils = { type FileInputUtils = {
clearFileInput?: () => void; clearFileInput?: () => void;
@ -59,6 +60,7 @@ type ImageUploadProps = DetailedHTMLProps<
existingFile?: FileInputToBase64FunctionReturn; existingFile?: FileInputToBase64FunctionReturn;
existingFileUrl?: string; existingFileUrl?: string;
icon?: ReactNode; icon?: ReactNode;
externalSetFileURL?: React.Dispatch<string | undefined>;
labelSpanProps?: ComponentProps<typeof Span>; labelSpanProps?: ComponentProps<typeof Span>;
loading?: boolean; loading?: boolean;
multiple?: boolean; multiple?: boolean;
@ -86,6 +88,7 @@ export default function FileUpload({
multiple, multiple,
onClear, onClear,
changeHandler, changeHandler,
externalSetFileURL,
...props ...props
}: ImageUploadProps) { }: ImageUploadProps) {
const [file, setFile] = React.useState< const [file, setFile] = React.useState<
@ -97,6 +100,7 @@ export default function FileUpload({
const [fileDraggedOver, setFileDraggedOver] = React.useState(false); const [fileDraggedOver, setFileDraggedOver] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const tempFileURLRef = React.useRef<string>(null);
React.useEffect(() => { React.useEffect(() => {
if (existingFileUrl) { if (existingFileUrl) {
@ -244,6 +248,7 @@ export default function FileUpload({
className="w-full relative h-full items-center justify-center overflow-hidden" className="w-full relative h-full items-center justify-center overflow-hidden"
{...previewImageWrapperProps} {...previewImageWrapperProps}
> >
<Stack className="w-full">
{disablePreview ? ( {disablePreview ? (
<Span className="opacity-50" size="small"> <Span className="opacity-50" size="small">
Image Uploaded! Image Uploaded!
@ -264,6 +269,16 @@ export default function FileUpload({
{...previewImageProps} {...previewImageProps}
/> />
)} )}
<Tag
variant="outlined"
color="gray"
className="w-full py-2 text-sm"
>
{fileUrl}
</Tag>
</Stack>
<Button <Button
variant="ghost" variant="ghost"
className={twMerge( className={twMerge(
@ -345,6 +360,59 @@ export default function FileUpload({
> >
{label || "Click to Upload File"} {label || "Click to Upload File"}
</Span> </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> </Stack>
</Center> </Center>
</Card> </Card>

View File

@ -207,14 +207,16 @@ export default function Input<KeyType extends string>(
window.clearTimeout(timeout); window.clearTimeout(timeout);
if (validationRegex && !validationFunction) { if (validationRegex) {
timeout = setTimeout(() => { timeout = setTimeout(() => {
setValidity({ setValidity({
isValid: validationRegex.test(val), isValid: validationRegex.test(val),
msg: "Value mismatch", msg: "Value mismatch",
}); });
}, finalDebounce); }, finalDebounce);
} else if (validationFunction) { }
if (validationFunction) {
window.clearTimeout(validationFnTimeout); window.clearTimeout(validationFnTimeout);
validationFnTimeout = setTimeout(() => { validationFnTimeout = setTimeout(() => {

View File

@ -232,9 +232,9 @@ export default function Select<
} }
hoverOpen hoverOpen
> >
<Card className="min-w-[250px] text-sm p-6"> <Card className="min-w-[250px] p-6">
{typeof info == "string" ? ( {typeof info == "string" ? (
<Span className="text-sm">{info}</Span> <Span>{info}</Span>
) : ( ) : (
info info
)} )}

View File

@ -258,7 +258,7 @@ export default function Button({
props.disabled ? "opacity-40 cursor-not-allowed" : "", props.disabled ? "opacity-40 cursor-not-allowed" : "",
"twui-button-general", "twui-button-general",
size == "small" size == "small"
? "px-3 py-1.5 text-sm twui-button-small" ? "px-3 py-1.5 twui-button-small text-sm"
: size == "smaller" : size == "smaller"
? "px-2 py-1 text-xs twui-button-smaller" ? "px-2 py-1 text-xs twui-button-smaller"
: size == "large" : size == "large"

View File

@ -12,7 +12,7 @@ export default function Container({
<div <div
{...props} {...props}
className={twMerge( 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", "flex-wrap flex-col xl:flex-row items-start xl:items-center",
"twui-container", "twui-container",
props.className props.className

View File

@ -12,7 +12,7 @@ export default function H5({
<h5 <h5
{...props} {...props}
className={twMerge( className={twMerge(
"text-sm mb-4", "mb-4",
"twui-headings twui-heading", "twui-headings twui-heading",
"twui-h5", "twui-h5",
props.className props.className

View File

@ -18,7 +18,7 @@ export default function Spacer({ horizontal, ...props }: Props) {
<div <div
{...props} {...props}
className={twMerge( className={twMerge(
"grow", "",
horizontal ? "w-10" : "w-full h-10", horizontal ? "w-10" : "w-full h-10",
"twui-spacer", "twui-spacer",
props.className props.className

View File

@ -23,6 +23,7 @@ export function useMDXComponents(params?: Params) {
pre: ({ children, ...props }) => { pre: ({ children, ...props }) => {
if (React.isValidElement(children) && children.props) { if (React.isValidElement(children) && children.props) {
return ( return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}> <CodeBlock {...props} backgroundColor={codeBgColor}>
{/* @ts-ignore */} {/* @ts-ignore */}
{children.props.children} {children.props.children}
@ -30,6 +31,7 @@ export function useMDXComponents(params?: Params) {
); );
} }
return ( return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}> <CodeBlock {...props} backgroundColor={codeBgColor}>
{children} {children}
</CodeBlock> </CodeBlock>
@ -62,6 +64,7 @@ export function useMDXComponents(params?: Params) {
); );
}, },
img: (props) => ( img: (props) => (
// @ts-ignore
<img <img
{...props} {...props}
className="w-full h-auto shadow-lg rounded-default overflow-hidden" className="w-full h-auto shadow-lg rounded-default overflow-hidden"

View File

@ -22,6 +22,7 @@ export default function useMDXComponents({
pre: ({ children, ...props }) => { pre: ({ children, ...props }) => {
if (React.isValidElement(children) && children.props) { if (React.isValidElement(children) && children.props) {
return ( return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}> <CodeBlock {...props} backgroundColor={codeBgColor}>
{/* @ts-ignore */} {/* @ts-ignore */}
{children.props.children} {children.props.children}
@ -29,6 +30,7 @@ export default function useMDXComponents({
); );
} }
return ( return (
// @ts-ignore
<CodeBlock {...props} backgroundColor={codeBgColor}> <CodeBlock {...props} backgroundColor={codeBgColor}>
{children} {children}
</CodeBlock> </CodeBlock>

View File

@ -1,17 +1,20 @@
import _ from "lodash"; 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 }> = { type FetchApiOptions<T extends { [k: string]: any } = { [k: string]: any }> = {
method: method: (typeof FetchAPIMethods)[number];
| "POST"
| "GET"
| "DELETE"
| "PUT"
| "PATCH"
| "post"
| "get"
| "delete"
| "put"
| "patch";
body?: T | string; body?: T | string;
headers?: FetchHeader; headers?: FetchHeader;
}; };

View File

@ -35,7 +35,7 @@ export default async function fileInputToBase64({
resolve(reader.result?.toString()); resolve(reader.result?.toString());
}; };
reader.onerror = function (/** @type {*} */ error: any) { reader.onerror = function (/** @type {*} */ error: any) {
console.log("Error: ", error.message); console.log("File Input to Base64 Error: ", error.message);
}; };
} }
); );

View File

@ -4,14 +4,28 @@ import Header from "./Header";
import Footer from "./Footer"; import Footer from "./Footer";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import MobileMenu from "./(sections)/MobileMenu"; 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); const [menuOpen, setMenuOpen] = React.useState(false);
return ( return (
<div className="flex flex-row items-stretch w-full min-h-screen"> <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 /> <Aside />
<div className={twMerge("flex flex-col items-start gap-0", "grow")}> <div className={twMerge("flex flex-col items-start gap-0", "grow")}>
<Header {...{ menuOpen, setMenuOpen }} /> <Header {...{ menuOpen, setMenuOpen }} />

View File

@ -3,7 +3,7 @@ import Main from "@/components/pages/about";
export default function ContactPage() { export default function ContactPage() {
return ( return (
<Layout> <Layout meta={{ title: "About Me | Tben.me" }}>
<Main /> <Main />
</Layout> </Layout>
); );

View File

@ -3,7 +3,7 @@ import Main from "@/components/pages/contact";
export default function ContactPage() { export default function ContactPage() {
return ( return (
<Layout> <Layout meta={{ title: "Contact Me | Tben.me" }}>
<Main /> <Main />
</Layout> </Layout>
); );

View File

@ -1,5 +1,4 @@
import Layout from "@/layouts/main"; import Layout from "@/layouts/main";
import H1 from "@/components/lib/layout/H1";
import Main from "@/components/pages/Home"; import Main from "@/components/pages/Home";
import AboutSection from "@/components/pages/Home/(sections)/AboutSection"; import AboutSection from "@/components/pages/Home/(sections)/AboutSection";
import Divider from "@/components/lib/layout/Divider"; import Divider from "@/components/lib/layout/Divider";
@ -8,7 +7,12 @@ import MyWorkSection from "@/components/pages/Home/(sections)/MyWorkSection";
export default function Home() { export default function Home() {
return ( return (
<Layout> <Layout
meta={{
description:
"Software Engineer, DevOps Engineer, Full Stack Developer, Software Architect, Philosopher, Solar Energy Enthusiast.",
}}
>
<Main /> <Main />
<Divider /> <Divider />
<AboutSection /> <AboutSection />

View File

@ -5,7 +5,7 @@ import MySkillsSection from "@/components/pages/Home/(sections)/MySkillsSection"
export default function SkillsPage() { export default function SkillsPage() {
return ( return (
<Layout> <Layout meta={{ title: "My Skills | Tben.me" }}>
<Main /> <Main />
<Divider /> <Divider />
<MySkillsSection noTitle expand /> <MySkillsSection noTitle expand />

View File

@ -5,7 +5,7 @@ import MyWorkSection from "@/components/pages/Home/(sections)/MyWorkSection";
export default function WorkPage() { export default function WorkPage() {
return ( return (
<Layout> <Layout meta={{ title: "My Work | Tben.me" }}>
<Main /> <Main />
<Divider /> <Divider />
<MyWorkSection noTitle expand /> <MyWorkSection noTitle expand />