From 57f1ecf5c3f4163291656d1114e3ff0b387d5ed5 Mon Sep 17 00:00:00 2001 From: Benjamin Toby Date: Tue, 3 Mar 2026 05:36:56 +0100 Subject: [PATCH] Updates --- .gitignore | 3 +- components/lib/editors/TinyMCE/index.tsx | 28 ++- components/lib/hooks/useStatus.tsx | 6 +- components/lib/layout/Button.tsx | 6 +- .../lib/mdx/markdown/MarkdownEditor.tsx | 22 ++- components/pages/Home/(data)/work.ts | 13 +- scripts/substack/save-all-notes.ts | 159 ++++++++++++++++++ 7 files changed, 217 insertions(+), 20 deletions(-) create mode 100644 scripts/substack/save-all-notes.ts diff --git a/.gitignore b/.gitignore index 8fd41ae..1e4e4ab 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts -dsql-schema-to-typedef.json \ No newline at end of file +dsql-schema-to-typedef.json +.data \ No newline at end of file diff --git a/components/lib/editors/TinyMCE/index.tsx b/components/lib/editors/TinyMCE/index.tsx index 667382a..5f9b885 100644 --- a/components/lib/editors/TinyMCE/index.tsx +++ b/components/lib/editors/TinyMCE/index.tsx @@ -24,6 +24,7 @@ export type TinyMCEEditorProps = { showLabel?: boolean; useParentCSS?: boolean; placeholder?: string; + refreshDependencyArray?: any[]; }; /** @@ -32,7 +33,7 @@ export type TinyMCEEditorProps = { */ export default function TinyMCEEditor({ options, - editorRef, + editorRef: passedEditorRef, setEditor: passedSetEditor, wrapperProps, defaultValue, @@ -43,10 +44,12 @@ export default function TinyMCEEditor({ showLabel, useParentCSS, placeholder, + refreshDependencyArray, }: TinyMCEEditorProps) { const { tinyMCE } = useTinyMCE(); const editorComponentRef = React.useRef(null); + const editorRef = passedEditorRef || React.useRef(null); const EDITOR_VALUE_CHANGE_TIMEOUT = 500; @@ -97,7 +100,10 @@ export default function TinyMCEEditor({ "body { font-family:Helvetica,Arial,sans-serif; font-size:14px; background-color: transparent }", init_instance_callback: (editor) => { setEditor(editor as any); - if (editorRef) editorRef.current = editor as any; + if (editorRef) { + editorRef.current = editor; + passedSetEditor?.(editor); + } if (defaultValue) editor.setContent(defaultValue); setReady(true); @@ -140,7 +146,15 @@ export default function TinyMCEEditor({ : undefined; instance?.remove(); }; - }, [tinyMCE, themeReady, refresh]); + }, [tinyMCE, themeReady, refresh, ...(refreshDependencyArray || [])]); + + React.useEffect(() => { + const instance = editorRef.current; + + if (instance) { + instance.setContent(defaultValue || ""); + } + }, [defaultValue]); return (
({ className={twMerge( "relative w-full [&_.tox-tinymce]:!border-none", "bg-background-light dark:bg-background-dark", - wrapperWrapperProps?.className + wrapperWrapperProps?.className, )} onInput={(e) => { console.log(`Input Detected`); @@ -159,7 +173,7 @@ export default function TinyMCEEditor({ className={twMerge( "absolute z-10 -top-[7px] left-[10px] px-2 text-xs", "bg-background-light dark:bg-background-dark text-gray-500", - "dark:text-white/80 rounded" + "dark:text-white/80 rounded", )} htmlFor={id} > @@ -170,7 +184,7 @@ export default function TinyMCEEditor({ {...borderProps} className={twMerge( "dark:border-white/30 p-0 pt-2", - borderProps?.className + borderProps?.className, )} >
({ }} className={twMerge( "bg-slate-200 dark:bg-slate-700 rounded-sm w-full", - "twui-rte-wrapper" + "twui-rte-wrapper", )} id={id} >
diff --git a/components/lib/hooks/useStatus.tsx b/components/lib/hooks/useStatus.tsx index 2f90521..db1ca1a 100644 --- a/components/lib/hooks/useStatus.tsx +++ b/components/lib/hooks/useStatus.tsx @@ -3,6 +3,7 @@ import React from "react"; type Params = { initialLoading?: boolean; initialReady?: boolean; + initialOpen?: boolean; }; export type UseStatusStatusType = { @@ -13,10 +14,11 @@ export type UseStatusStatusType = { export default function useStatus(params?: Params) { const [refresh, setRefresh] = React.useState(0); const [loading, setLoading] = React.useState( - params?.initialLoading || false + params?.initialLoading || false, ); const [status, setStatus] = React.useState({}); const [ready, setReady] = React.useState(params?.initialReady || false); + const [open, setOpen] = React.useState(params?.initialOpen || false); return { refresh, @@ -27,5 +29,7 @@ export default function useStatus(params?: Params) { setStatus, ready, setReady, + open, + setOpen, }; } diff --git a/components/lib/layout/Button.tsx b/components/lib/layout/Button.tsx index 715300c..98f3919 100644 --- a/components/lib/layout/Button.tsx +++ b/components/lib/layout/Button.tsx @@ -1,4 +1,4 @@ -import { +import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, ComponentProps, @@ -35,8 +35,8 @@ export type TWUIButtonProps = DetailedHTMLProps< AnchorHTMLAttributes, HTMLAnchorElement >; - beforeIcon?: TWUILucideIconName | JSX.Element; - afterIcon?: TWUILucideIconName | JSX.Element; + beforeIcon?: TWUILucideIconName | React.JSX.Element; + afterIcon?: TWUILucideIconName | React.JSX.Element; buttonContentProps?: DetailedHTMLProps< HTMLAttributes, HTMLDivElement diff --git a/components/lib/mdx/markdown/MarkdownEditor.tsx b/components/lib/mdx/markdown/MarkdownEditor.tsx index 2282f55..a0ac2cd 100644 --- a/components/lib/mdx/markdown/MarkdownEditor.tsx +++ b/components/lib/mdx/markdown/MarkdownEditor.tsx @@ -1,4 +1,4 @@ -import React, { ComponentProps } from "react"; +import React, { ComponentProps, useRef } from "react"; import { twMerge } from "tailwind-merge"; import MarkdownEditorPreviewComponent from "./MarkdownEditorPreviewComponent"; import MarkdownEditorComponent from "./MarkdownEditorComponent"; @@ -6,6 +6,7 @@ import MarkdownEditorSelectorButtons from "./MarkdownEditorSelectorButtons"; import Row from "../../layout/Row"; import Stack from "../../layout/Stack"; import AceEditor from "../../editors/AceEditor"; +import useStatus from "../../hooks/useStatus"; type Props = { value?: string; @@ -29,17 +30,30 @@ export default function MarkdownEditor({ const [value, setValue] = React.useState(existingValue || ``); const [sideBySide, setSideBySide] = React.useState( - defaultSideBySide || false + defaultSideBySide || false, ); const [preview, setPreview] = React.useState(false); + const { refresh, setRefresh } = useStatus(); + const updatingFromExtValueRef = useRef(false); const maxHeight = existingMaxHeight || "600px"; React.useEffect(() => { + if (updatingFromExtValueRef.current) return; setExistingValue?.(value); changeHandler?.(value); }, [value]); + React.useEffect(() => { + if (!existingValue) return; + updatingFromExtValueRef.current = true; + setValue(existingValue); + setTimeout(() => { + updatingFromExtValueRef.current = false; + setRefresh((prev) => prev + 1); + }, 500); + }, [existingValue]); + return ( {!noToggleButtons && ( @@ -52,13 +66,14 @@ export default function MarkdownEditor({ )} diff --git a/components/pages/Home/(data)/work.ts b/components/pages/Home/(data)/work.ts index 1a72a1e..ac69f1d 100644 --- a/components/pages/Home/(data)/work.ts +++ b/components/pages/Home/(data)/work.ts @@ -25,11 +25,7 @@ export const work = { description: "Mortgage Broker in Utah", href: "https://summitlending.com", image: "/images/work/devops/server-management.png", - metrics: [ - "500+ leads/month", - "~10 deploys/week", - "99.99% uptime", - ], + metrics: ["500+ leads/month", "99.99% uptime"], technologies: [ "Next JS", "Tailwind CSS", @@ -142,6 +138,13 @@ export const work = { "Azure", ], }, + { + title: "Bun SQLite", + description: + "A schema-driven SQLite manager for Bun, featuring automatic schema synchronization, type-safe CRUD operations, vector embedding support, and TypeScript type definition generation.", + href: "https://git.tben.me/Moduletrace/bun-sqlite", + technologies: ["SQLite", "Bun", "Shell", "Typescript"], + }, { title: "Turbo Sync", description: diff --git a/scripts/substack/save-all-notes.ts b/scripts/substack/save-all-notes.ts new file mode 100644 index 0000000..7d9d4b6 --- /dev/null +++ b/scripts/substack/save-all-notes.ts @@ -0,0 +1,159 @@ +// @ts-ignore +const articles_objects = []; + +const substack_href = "https://substack.com/@benjamintoby"; + +// @ts-ignore +async function sleep(wait) { + return new Promise((res) => { + setTimeout(() => { + res(true); + }, wait); + }); +} + +function grabContentHeight() { + return document.querySelector( + "div[style='max-width: 568px;']", + // @ts-ignore + )?.offsetHeight; +} + +async function scrollToEnd() { + let last_content_height = grabContentHeight(); + + while (true) { + window.scrollTo({ + top: document.body.scrollHeight, + behavior: "smooth", + }); + + await sleep(5000); + + const current_content_height = grabContentHeight(); + + if (current_content_height > last_content_height) { + last_content_height = current_content_height; + } else { + break; + } + } +} + +async function main() { + await scrollToEnd(); + + const articles = Array.from( + document.querySelectorAll("div[role='article']"), + ); + + console.log(`Handling ${articles.length} Articles ...`); + + for (let i = 0; i < articles.length; i++) { + let present_articles = Array.from( + document.querySelectorAll("div[role='article']"), + ); + + console.log(`Found ${present_articles.length} Present Articles!`); + + while (i > present_articles.length - 1) { + window.scrollTo({ + top: document.body.scrollHeight, + behavior: "smooth", + }); + + console.log(`Searching for Article #${i} ...`); + + await sleep(5000); + + present_articles = Array.from( + document.querySelectorAll("div[role='article']"), + ); + } + + const article = present_articles[i]; + + console.log(`Handling Article #${i} ...`); + + const content_div = article.querySelector(`.FeedProseMirror`); + const date_link = Array.from(article.querySelectorAll("a")).find((a) => + Boolean(a.getAttribute("title")), + ); + + if (!content_div) continue; + + const content_div_first_paragraph = content_div.querySelector(`p`); + + if (!content_div_first_paragraph) continue; + + let window_url = window.location.href; + const initial_text_content = content_div.textContent; + + const article_object = { + title: initial_text_content, + content: initial_text_content, + html: content_div.innerHTML, + images: [], + date: date_link?.getAttribute("title"), + }; + + const article_images = Array.from( + article.querySelectorAll("picture img"), + ); + + if (article_images?.[0]) { + for (let img = 0; img < article_images.length; img++) { + if (img > 0) { + const article_image = article_images[img]; + // @ts-ignore + const article_image_srcset = article_image.srcset; + const largest_image = article_image_srcset + .split(` `) + .at(-2); + // @ts-ignore + article_object.images.push(largest_image); + } + } + } + + const more_content = Array.from(article.querySelectorAll("a")) + .find((el) => el.textContent.includes("See more")) + ?.click(); + + await sleep(2000); + + let new_window_url = window.location.href; + + if (new_window_url === window_url) { + const new_article_content = + article.querySelector(`.FeedProseMirror`); + + if (new_article_content) { + article_object.content = new_article_content.textContent; + article_object.html = new_article_content.innerHTML; + } + + articles_objects.push(article_object); + } else { + const text_sample = content_div_first_paragraph.textContent; + const target_content_div = Array.from( + document.querySelectorAll(".ProseMirror.FeedProseMirror"), + ).find((el) => el.textContent.includes(text_sample)); + + if (target_content_div) { + article_object.content = target_content_div.textContent; + article_object.html = target_content_div.innerHTML; + } + + articles_objects.push(article_object); + + window.history.back(); + await sleep(2000); + } + } + + // @ts-ignore + console.log(articles_objects); +} + +main();