From 0b7c70058d211a528b77b7e043d0ed315f865b00 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 2 Dec 2025 16:30:46 +0100 Subject: [PATCH] Updates --- .../lib/(partials)/PopoverComponent.tsx | 2 +- components/lib/base.css | 15 +++ .../lib/composites/docs/TWUIDocsLink.tsx | 2 +- components/lib/editors/AceEditor.tsx | 4 +- components/lib/editors/TinyMCE/index.tsx | 78 +++++++++---- components/lib/editors/TinyMCE/useTinyMCE.tsx | 49 +++++--- components/lib/elements/Dropdown.tsx | 4 + components/lib/elements/LoadingOverlay.tsx | 14 ++- components/lib/elements/Modal.tsx | 9 +- components/lib/elements/Search.tsx | 6 +- components/lib/elements/Table.tsx | 4 +- components/lib/elements/Tabs.tsx | 9 +- components/lib/elements/ai/AIPromptBlock.tsx | 12 +- .../lib/elements/ai/AIPromptPreview.tsx | 4 +- components/lib/form/FileUpload.tsx | 108 ++++++++++++++---- components/lib/form/Input/index.tsx | 6 +- components/lib/form/Select.tsx | 4 +- components/lib/layout/Button.tsx | 2 +- components/lib/layout/Container.tsx | 2 +- components/lib/layout/H5.tsx | 2 +- components/lib/layout/Spacer.tsx | 2 +- components/lib/mdx/mdx-components.tsx | 3 + .../lib/next-js/hooks/useMDXComponents.tsx | 2 + components/lib/utils/fetch/fetchApi.ts | 25 ++-- .../lib/utils/form/fileInputToBase64.ts | 2 +- layouts/main/index.tsx | 18 ++- pages/about.tsx | 2 +- pages/contact.tsx | 2 +- pages/index.tsx | 8 +- pages/skills.tsx | 2 +- pages/work.tsx | 2 +- 31 files changed, 298 insertions(+), 106 deletions(-) diff --git a/components/lib/(partials)/PopoverComponent.tsx b/components/lib/(partials)/PopoverComponent.tsx index 1da18d3..e75658d 100644 --- a/components/lib/(partials)/PopoverComponent.tsx +++ b/components/lib/(partials)/PopoverComponent.tsx @@ -69,7 +69,7 @@ export default function PopoverComponent({ diff --git a/components/lib/editors/AceEditor.tsx b/components/lib/editors/AceEditor.tsx index be185e8..f0acb4a 100644 --- a/components/lib/editors/AceEditor.tsx +++ b/components/lib/editors/AceEditor.tsx @@ -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 ( - + Editor Error:{" "} {error.message} diff --git a/components/lib/editors/TinyMCE/index.tsx b/components/lib/editors/TinyMCE/index.tsx index 7d8c7b1..667382a 100644 --- a/components/lib/editors/TinyMCE/index.tsx +++ b/components/lib/editors/TinyMCE/index.tsx @@ -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 = { - tinyMCE?: TinyMCE | null; options?: RawEditorOptions; editorRef?: React.MutableRefObject; setEditor?: React.Dispatch>; @@ -26,8 +26,6 @@ export type TinyMCEEditorProps = { 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({ options, editorRef, - setEditor, - tinyMCE, + setEditor: passedSetEditor, wrapperProps, defaultValue, changeHandler, @@ -47,29 +44,49 @@ export default function TinyMCEEditor({ useParentCSS, placeholder, }: TinyMCEEditorProps) { + const { tinyMCE } = useTinyMCE(); + const editorComponentRef = React.useRef(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(); 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({ 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({ }); 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 (
({ "bg-background-light dark:bg-background-dark text-gray-500", "dark:text-white/80 rounded" )} - htmlFor={name || "twui-tinymce"} + htmlFor={id} > {title} @@ -153,7 +185,7 @@ export default function TinyMCEEditor({ "bg-slate-200 dark:bg-slate-700 rounded-sm w-full", "twui-rte-wrapper" )} - id={name || "twui-tinymce"} + id={id} >
diff --git a/components/lib/editors/TinyMCE/useTinyMCE.tsx b/components/lib/editors/TinyMCE/useTinyMCE.tsx index 6480d4e..dadedb4 100644 --- a/components/lib/editors/TinyMCE/useTinyMCE.tsx +++ b/components/lib/editors/TinyMCE/useTinyMCE.tsx @@ -4,32 +4,49 @@ import { TinyMCE } from "./tinymce"; let interval: any; export default function useTinyMCE() { - const [tinyMCE, setTinyMCE] = React.useState(null); + const [tinyMCE, setTinyMCE] = React.useState(); + 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 }; } diff --git a/components/lib/elements/Dropdown.tsx b/components/lib/elements/Dropdown.tsx index 92b2729..1d08a0b 100644 --- a/components/lib/elements/Dropdown.tsx +++ b/components/lib/elements/Dropdown.tsx @@ -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", diff --git a/components/lib/elements/LoadingOverlay.tsx b/components/lib/elements/LoadingOverlay.tsx index 8a2575d..e9e8772 100644 --- a/components/lib/elements/LoadingOverlay.tsx +++ b/components/lib/elements/LoadingOverlay.tsx @@ -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 > & { loadingProps?: ComponentProps; + 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 (
- + + + {label && {label}} +
); diff --git a/components/lib/elements/Modal.tsx b/components/lib/elements/Modal.tsx index 68e3d67..2ca1783 100644 --- a/components/lib/elements/Modal.tsx +++ b/components/lib/elements/Modal.tsx @@ -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 } diff --git a/components/lib/elements/Search.tsx b/components/lib/elements/Search.tsx index dca6430..180ada6 100644 --- a/components/lib/elements/Search.tsx +++ b/components/lib/elements/Search.tsx @@ -21,6 +21,7 @@ export type SearchProps = DetailedHTMLProps< >; loading?: boolean; placeholder?: string; + componentRef?: React.RefObject; }; /** @@ -37,6 +38,7 @@ export default function Search({ buttonProps, loading, placeholder, + componentRef, ...props }: SearchProps) { const [input, setInput] = React.useState( @@ -52,7 +54,7 @@ export default function Search({ }, delay); }, [input]); - const inputRef = React.useRef(null); + const inputRef = componentRef || React.useRef(null); React.useEffect(() => { if (props.autoFocus) { @@ -64,7 +66,7 @@ export default function Search({ & { * @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({ )} > {values.map((value, index) => { diff --git a/components/lib/elements/ai/AIPromptBlock.tsx b/components/lib/elements/ai/AIPromptBlock.tsx index a612e6e..2fe756e 100644 --- a/components/lib/elements/ai/AIPromptBlock.tsx +++ b/components/lib/elements/ai/AIPromptBlock.tsx @@ -18,6 +18,7 @@ type Props = { loading?: boolean; mdRes?: string; setMdRes: React.Dispatch>; + 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({ @@ -53,11 +55,15 @@ export default function AIPromptBlock({ setStreamRes={setMdRes} streamRes={mdRes} history={history} + loading={loading} /> {loading && }