From 6e0ef96ccbd9085b58515b4750917981d77c1c08 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Wed, 9 Sep 2026 18:49:44 +1200 Subject: [PATCH 01/37] Set default values on function parameters for fetchThemeBundle --- shared/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/helpers.js b/shared/helpers.js index 667d7b6b1..db046b810 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -146,7 +146,7 @@ const printCurrentBrew = async ()=>{ } }; -const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ +const fetchThemeBundle = async (setError = ()=>{}, setThemeBundle = ()=>{}, renderer, theme)=>{ if(!renderer || !theme) return; const res = await request .get(`/api/theme/${renderer}/${theme}`) From 3ccb4e5ceae29a369addb86399104f47ef749c0b Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 11:35:31 -0400 Subject: [PATCH 02/37] Remove default ver prop from homePage.jsx Only used in the navbar component which internally already uses a default value. Edit and New already have this removed. --- client/homebrew/pages/homePage/homePage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 4dc505ea9..3ae1c50a4 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -41,7 +41,6 @@ const neverSaved = true; const HomePage =(props)=>{ props = { brew : DEFAULT_BREW, - ver : '0.0.0', ...props }; From 15a20079dd2f6a7ec095aed7cddc4fa2e0865b17 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 11:37:25 -0400 Subject: [PATCH 03/37] Remove commented "chrome warning" from navbar.jsx Warning when non-chrome browser is detected already moved to a different file. No need to keep this commented-out chunk left here in navbar.jsx. --- client/homebrew/navbar/navbar.jsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client/homebrew/navbar/navbar.jsx b/client/homebrew/navbar/navbar.jsx index db9a836c9..40e5aef71 100644 --- a/client/homebrew/navbar/navbar.jsx +++ b/client/homebrew/navbar/navbar.jsx @@ -9,22 +9,10 @@ const Navbar = createReactClass({ displayName : 'Navbar', getInitialState : function() { return { - // showNonChromeWarning: false, // uncomment if needed ver : global.version || '0.0.0' }; }, - /* - renderChromeWarning : function(){ - if(!this.state.showNonChromeWarning) return; - return - Optimized for Chrome -
- If you are experiencing rendering issues, use Chrome instead -
-
- }, -*/ render : function(){ return From 5865e5adfd93f98b1360b3da1a3abef4700672b9 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 18:42:39 +0000 Subject: [PATCH 04/37] Extract common "handlePageChange" to a reusable hook --- client/homebrew/pages/editPage/editPage.jsx | 61 +++++++++---------- client/homebrew/pages/homePage/homePage.jsx | 58 ++++++++---------- client/homebrew/pages/newPage/newPage.jsx | 58 ++++++++---------- .../homebrew/utils/commonEditPageFunctions.js | 43 +++++++++++++ package-lock.json | 15 ----- 5 files changed, 125 insertions(+), 110 deletions(-) create mode 100644 client/homebrew/utils/commonEditPageFunctions.js diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 36df0f1bb..29f215e08 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -10,18 +10,20 @@ import _ from 'lodash'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' + import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -import Nav from '@navbar/nav.jsx'; -import Navbar from '@navbar/navbar.jsx'; -import NewBrewItem from '@navbar/newbrew.navitem.jsx'; -import AccountNavItem from '@navbar/account.navitem.jsx'; -import ErrorNavItem from '@navbar/error-navitem.jsx'; -import HelpNavItem from '@navbar/help.navitem.jsx'; -import VaultNavItem from '@navbar/vault.navitem.jsx'; -import PrintNavItem from '@navbar/print.navitem.jsx'; +import Nav from '@navbar/nav.jsx'; +import Navbar from '@navbar/navbar.jsx'; +import NewBrewItem from '@navbar/newbrew.navitem.jsx'; +import AccountNavItem from '@navbar/account.navitem.jsx'; +import ErrorNavItem from '@navbar/error-navitem.jsx'; +import HelpNavItem from '@navbar/help.navitem.jsx'; +import VaultNavItem from '@navbar/vault.navitem.jsx'; +import PrintNavItem from '@navbar/print.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx'; const { both: RecentNavItem } = RecentNavItems; @@ -41,7 +43,6 @@ const SAVE_TIMEOUT = 10000; const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds - const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; @@ -49,7 +50,7 @@ const SNIPKEY = 'HB_newPage_snippets'; const METAKEY = 'HB_newPage_meta'; const useLocalStorage = false; -const neverSaved = false; +const neverSaved = false; const EditPage = (props)=>{ props = { @@ -82,6 +83,23 @@ const EditPage = (props)=>{ const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges + const { + handleBrewChange + } = useCommonEditPageFunctions({ + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + fetchThemeBundle, + hbfm + }); + useEffect(()=>{ const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); setAutoSaveEnabled(autoSavePref); @@ -126,29 +144,6 @@ const EditPage = (props)=>{ editorRef.current?.update(); }; - const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' - if(subfield == 'renderer' || subfield == 'theme') - fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); - - //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length && (field == 'text' || field == 'snippets')) - setHTMLErrors(hbfm.validate(value)); - - if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); - else setCurrentBrew((prev)=>({ ...prev, [field]: value })); - - if(useLocalStorage) { - if(field == 'text') localStorage.setItem(BREWKEY, value); - if(field == 'style') localStorage.setItem(STYLEKEY, value); - if(field == 'snippets') localStorage.setItem(SNIPKEY, value); - if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ - renderer : value.renderer, - theme : value.theme, - lang : value.lang - })); - } - }; - const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ ...prevBrew, style : newData.style, diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 4dc505ea9..34c2937f1 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -10,18 +10,20 @@ import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' + import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -import Nav from '@navbar/nav.jsx'; -import Navbar from '@navbar/navbar.jsx'; -import NewBrewItem from '@navbar/newbrew.navitem.jsx'; -import AccountNavItem from '@navbar/account.navitem.jsx'; -import ErrorNavItem from '@navbar/error-navitem.jsx'; -import HelpNavItem from '@navbar/help.navitem.jsx'; -import VaultNavItem from '@navbar/vault.navitem.jsx'; -import PrintNavItem from '@navbar/print.navitem.jsx'; +import Nav from '@navbar/nav.jsx'; +import Navbar from '@navbar/navbar.jsx'; +import NewBrewItem from '@navbar/newbrew.navitem.jsx'; +import AccountNavItem from '@navbar/account.navitem.jsx'; +import ErrorNavItem from '@navbar/error-navitem.jsx'; +import HelpNavItem from '@navbar/help.navitem.jsx'; +import VaultNavItem from '@navbar/vault.navitem.jsx'; +import PrintNavItem from '@navbar/print.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx'; const { both: RecentNavItem } = RecentNavItems; @@ -60,6 +62,23 @@ const HomePage =(props)=>{ const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const unsavedChangesRef = useRef(unsavedChanges); + const { + handleBrewChange + } = useCommonEditPageFunctions({ + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + fetchThemeBundle, + hbfm + }); + useEffect(()=>{ fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -112,29 +131,6 @@ const HomePage =(props)=>{ editorRef.current.update(); }; - const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' - if(subfield == 'renderer' || subfield == 'theme') - fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); - - //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length && (field == 'text' || field == 'snippets')) - setHTMLErrors(hbfm.validate(value)); - - if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); - else setCurrentBrew((prev)=>({ ...prev, [field]: value })); - - if(useLocalStorage) { - if(field == 'text') localStorage.setItem(BREWKEY, value); - if(field == 'style') localStorage.setItem(STYLEKEY, value); - if(field == 'snippets') localStorage.setItem(SNIPKEY, value); - if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ - renderer : value.renderer, - theme : value.theme, - lang : value.lang - })); - } - }; - const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING if(isSaving) diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index c45090fb7..e55434f5c 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -10,18 +10,20 @@ import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js'; +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' + import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -import Nav from '@navbar/nav.jsx'; -import Navbar from '@navbar/navbar.jsx'; -import NewBrewItem from '@navbar/newbrew.navitem.jsx'; -import AccountNavItem from '@navbar/account.navitem.jsx'; -import ErrorNavItem from '@navbar/error-navitem.jsx'; -import HelpNavItem from '@navbar/help.navitem.jsx'; -import VaultNavItem from '@navbar/vault.navitem.jsx'; -import PrintNavItem from '@navbar/print.navitem.jsx'; +import Nav from '@navbar/nav.jsx'; +import Navbar from '@navbar/navbar.jsx'; +import NewBrewItem from '@navbar/newbrew.navitem.jsx'; +import AccountNavItem from '@navbar/account.navitem.jsx'; +import ErrorNavItem from '@navbar/error-navitem.jsx'; +import HelpNavItem from '@navbar/help.navitem.jsx'; +import VaultNavItem from '@navbar/vault.navitem.jsx'; +import PrintNavItem from '@navbar/print.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx'; const { both: RecentNavItem } = RecentNavItems; @@ -61,6 +63,23 @@ const NewPage = (props)=>{ const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges + const { + handleBrewChange + } = useCommonEditPageFunctions({ + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + fetchThemeBundle, + hbfm + }); + useEffect(()=>{ loadBrew(); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -127,29 +146,6 @@ const NewPage = (props)=>{ editorRef.current.update(); }; - const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' - if(subfield == 'renderer' || subfield == 'theme') - fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); - - //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length && (field == 'text' || field == 'snippets')) - setHTMLErrors(hbfm.validate(value)); - - if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); - else setCurrentBrew((prev)=>({ ...prev, [field]: value })); - - if(useLocalStorage) { - if(field == 'text') localStorage.setItem(BREWKEY, value); - if(field == 'style') localStorage.setItem(STYLEKEY, value); - if(field == 'snippets') localStorage.setItem(SNIPKEY, value); - if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ - renderer : value.renderer, - theme : value.theme, - lang : value.lang - })); - } - }; - const trySave = async ()=>{ setIsSaving(true); diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js new file mode 100644 index 000000000..bf397e334 --- /dev/null +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -0,0 +1,43 @@ +export default function useCommonEditPageFunctions(dependencies) { + const { + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + fetchThemeBundle, + hbfm + } = dependencies; + + const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' + if(subfield == 'renderer' || subfield == 'theme') + fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); + + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length && (field == 'text' || field == 'snippets')) + setHTMLErrors(hbfm.validate(value)); + + if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); + else setCurrentBrew((prev)=>({ ...prev, [field]: value })); + + if(useLocalStorage) { + if(field == 'text') localStorage.setItem(BREWKEY, value); + if(field == 'style') localStorage.setItem(STYLEKEY, value); + if(field == 'snippets') localStorage.setItem(SNIPKEY, value); + if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ + renderer : value.renderer, + theme : value.theme, + lang : value.lang + })); + } + }; + + return { + handleBrewChange + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8309fba77..f7d138beb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12085,21 +12085,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/gcp-metadata": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", - "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", - "extraneous": true, - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/mongoose/node_modules/mongodb": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz", From 67728afd4f4e8038b8f353a3d594550bb3e2ab0b Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 21:08:58 +0000 Subject: [PATCH 05/37] Give "unsaved change" warning to NewPage and HomePage. Autosave is always disabled on those. --- client/homebrew/pages/editPage/editPage.jsx | 22 ++++---- client/homebrew/pages/homePage/homePage.jsx | 56 +++++++++++++------- client/homebrew/pages/newPage/newPage.jsx | 57 +++++++++++++++------ 3 files changed, 90 insertions(+), 45 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 29f215e08..f6f74c269 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -40,7 +40,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_TIMEOUT = 90000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; @@ -50,7 +50,7 @@ const SNIPKEY = 'HB_newPage_snippets'; const METAKEY = 'HB_newPage_meta'; const useLocalStorage = false; -const neverSaved = false; +const sandbox = false; const EditPage = (props)=>{ props = { @@ -101,7 +101,8 @@ const EditPage = (props)=>{ }); useEffect(()=>{ - const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); + const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); + setAutoSaveEnabled(autoSavePref); setWarnUnsavedChanges(!autoSavePref); setHTMLErrors(hbfm.validate(currentBrew.text)); @@ -122,6 +123,7 @@ const EditPage = (props)=>{ if(unsavedChangesRef.current) return 'You have unsaved changes!'; }; + return ()=>{ document.removeEventListener('keydown', handleControlKeys); window.onbeforeunload = null; @@ -313,12 +315,12 @@ const EditPage = (props)=>{ resetWarnUnsavedTimer(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 - ? 'Autosave is OFF.' - : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + ? `Autosave is OFF${sandbox ?? 'for this sandbox page'}.` + : `Autosave is OFF${sandbox ?? 'for this sandbox page'}, and you haven't saved for ${elapsedTime} minutes.`; return - Reminder... -
{text}
+ Reminder... +
{text}
; } @@ -330,9 +332,9 @@ const EditPage = (props)=>{ if(autoSaveEnabled) return auto-saved; - // #5 - No unsaved changes, and has never been saved, hide the button - if(neverSaved) - return save now; + // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button + if(sandbox) + return save now; // DEFAULT - No unsaved changes, show SAVED return saved; diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index f528f934f..c666703c3 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -32,13 +32,18 @@ const { both: RecentNavItem } = RecentNavItems; import Headtags from '@vitreum/headtags.js'; const Meta = Headtags.Meta; -const BREWKEY = 'homebrewery-new'; -const STYLEKEY = 'homebrewery-new-style'; -const SNIPKEY = 'homebrewery-new-snippets'; -const METAKEY = 'homebrewery-new-meta'; +const SAVE_TIMEOUT = 10000; +const UNSAVED_WARNING_TIMEOUT = 90000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds + +const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; +const BREWKEY = 'HB_newPage_content'; +const STYLEKEY = 'HB_newPage_style'; +const SNIPKEY = 'HB_newPage_snippets'; +const METAKEY = 'HB_newPage_meta'; const useLocalStorage = false; -const neverSaved = true; +const sandbox = true; const HomePage =(props)=>{ props = { @@ -55,7 +60,8 @@ const HomePage =(props)=>{ const [themeBundle, setThemeBundle] = useState({}); const [unsavedChanges, setUnsavedChanges] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [autoSaveEnabled, setAutoSaveEnable] = useState(false); + const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); + const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -79,6 +85,11 @@ const HomePage =(props)=>{ }); useEffect(()=>{ + const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); + + setAutoSaveEnabled(autoSavePref); + setWarnUnsavedChanges(!autoSavePref); + setHTMLErrors(hbfm.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); const handleControlKeys = (e)=>{ @@ -96,6 +107,7 @@ const HomePage =(props)=>{ if(unsavedChangesRef.current) return 'You have unsaved changes!'; }; + return ()=>{ document.removeEventListener('keydown', handleControlKeys); window.onbeforeunload = null; @@ -130,24 +142,30 @@ const HomePage =(props)=>{ editorRef.current.update(); }; + const resetWarnUnsavedTimer = ()=>{ + setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds + clearTimeout(warnUnsavedTimeout.current); + warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings + }; + const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING if(isSaving) return saving...; // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - // if(unsavedChanges && warnUnsavedChanges) { - // resetWarnUnsavedTimer(); - // const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - // const text = elapsedTime === 0 - // ? 'Autosave is OFF.' - // : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + if(unsavedChanges && warnUnsavedChanges) { + resetWarnUnsavedTimer(); + const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? `Autosave is OFF${sandbox ?? 'for this sandbox page'}.` + : `Autosave is OFF${sandbox ?? 'for this sandbox page'}, and you haven't saved for ${elapsedTime} minutes.`; - // return - // Reminder... - //
{text}
- //
; - // } + return + Reminder... +
{text}
+
; + } // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) @@ -157,8 +175,8 @@ const HomePage =(props)=>{ if(autoSaveEnabled) return auto-saved; - // #5 - No unsaved changes, and has never been saved, hide the button - if(neverSaved) + // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button + if(sandbox) return save now; // DEFAULT - No unsaved changes, show SAVED diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index e55434f5c..ab024fe7a 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -28,15 +28,20 @@ import RecentNavItems from '@navbar/recent.navitem.jsx'; const { both: RecentNavItem } = RecentNavItems; // Page specific imports +const SAVE_TIMEOUT = 10000; +const UNSAVED_WARNING_TIMEOUT = 90000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds +const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; -const METAKEY = 'HB_newPage_metadata'; const SNIPKEY = 'HB_newPage_snippets'; +const METAKEY = 'HB_newPage_meta'; + const SAVEKEYPREFIX = 'HB_editor_defaultSave_'; const useLocalStorage = true; -const neverSaved = true; +const sandbox = true; const NewPage = (props)=>{ props = { @@ -55,6 +60,7 @@ const NewPage = (props)=>{ const [themeBundle, setThemeBundle] = useState({}); const [unsavedChanges, setUnsavedChanges] = useState(false); const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); + const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -63,6 +69,10 @@ const NewPage = (props)=>{ const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges + useEffect(()=>{ + loadBrew(); + }, []); + const { handleBrewChange } = useCommonEditPageFunctions({ @@ -81,7 +91,11 @@ const NewPage = (props)=>{ }); useEffect(()=>{ - loadBrew(); + const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); + + setAutoSaveEnabled(autoSavePref); + setWarnUnsavedChanges(!autoSavePref); + setHTMLErrors(hbfm.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); const handleControlKeys = (e)=>{ @@ -95,9 +109,14 @@ const NewPage = (props)=>{ }; document.addEventListener('keydown', handleControlKeys); + window.onbeforeunload = ()=>{ + if(unsavedChangesRef.current) + return 'You have unsaved changes!'; + }; return ()=>{ document.removeEventListener('keydown', handleControlKeys); + window.onbeforeunload = null; }; }, []); @@ -146,6 +165,12 @@ const NewPage = (props)=>{ editorRef.current.update(); }; + const resetWarnUnsavedTimer = ()=>{ + setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds + clearTimeout(warnUnsavedTimeout.current); + warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings + }; + const trySave = async ()=>{ setIsSaving(true); @@ -180,18 +205,18 @@ const NewPage = (props)=>{ return saving...; // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - // if(unsavedChanges && warnUnsavedChanges) { - // resetWarnUnsavedTimer(); - // const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - // const text = elapsedTime === 0 - // ? 'Autosave is OFF.' - // : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + if(unsavedChanges && warnUnsavedChanges) { + resetWarnUnsavedTimer(); + const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? `Autosave is OFF${sandbox ?? 'for this sandbox page'}.` + : `Autosave is OFF${sandbox ?? 'for this sandbox page'}, and you haven't saved for ${elapsedTime} minutes.`; - // return - // Reminder... - //
{text}
- //
; - // } + return + Reminder... +
{text}
+
; + } // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) @@ -201,8 +226,8 @@ const NewPage = (props)=>{ if(autoSaveEnabled) return auto-saved; - // #5 - No unsaved changes, and has never been saved, hide the button - if(neverSaved) + // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button + if(sandbox) return save now; // DEFAULT - No unsaved changes, show SAVED From 76541cd836cb25a3b010826d06ef480c87fa49f5 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 21:21:00 +0000 Subject: [PATCH 06/37] Add missing timeout ref --- client/homebrew/pages/homePage/homePage.jsx | 1 + client/homebrew/pages/newPage/newPage.jsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index c666703c3..1240066a7 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -65,6 +65,7 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); + const warnUnsavedTimeout = useRef(null); const unsavedChangesRef = useRef(unsavedChanges); const { diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index ab024fe7a..b9450882f 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -65,7 +65,7 @@ const NewPage = (props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); // const saveTimeout = useRef(null); - // const warnUnsavedTimeout = useRef(null); + const warnUnsavedTimeout = useRef(null); const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges From 2c416dcdb90dbb0030cd87b792ade894a7453e02 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 21:23:50 +0000 Subject: [PATCH 07/37] Add missing lastSavedTime state --- client/homebrew/pages/homePage/homePage.jsx | 1 + client/homebrew/pages/newPage/newPage.jsx | 1 + 2 files changed, 2 insertions(+) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 1240066a7..34874d37c 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -60,6 +60,7 @@ const HomePage =(props)=>{ const [themeBundle, setThemeBundle] = useState({}); const [unsavedChanges, setUnsavedChanges] = useState(false); const [isSaving, setIsSaving] = useState(false); + const [lastSavedTime, setLastSavedTime] = useState(new Date()); const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index b9450882f..83e91b1a2 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -51,6 +51,7 @@ const NewPage = (props)=>{ const [currentBrew, setCurrentBrew] = useState(props.brew); const [isSaving, setIsSaving] = useState(false); + const [lastSavedTime, setLastSavedTime] = useState(new Date()); const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false); const [error, setError] = useState(null); const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text)); From 719b30a04ee01b19250c70309f57da2be04dd863 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 21:33:02 +0000 Subject: [PATCH 08/37] Fix message syntax --- client/homebrew/pages/editPage/editPage.jsx | 4 ++-- client/homebrew/pages/homePage/homePage.jsx | 4 ++-- client/homebrew/pages/newPage/newPage.jsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index f6f74c269..386f2afb0 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -315,8 +315,8 @@ const EditPage = (props)=>{ resetWarnUnsavedTimer(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ?? 'for this sandbox page'}.` - : `Autosave is OFF${sandbox ?? 'for this sandbox page'}, and you haven't saved for ${elapsedTime} minutes.`; + ? `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; return Reminder... diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 34874d37c..0c85fa48c 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -160,8 +160,8 @@ const HomePage =(props)=>{ resetWarnUnsavedTimer(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ?? 'for this sandbox page'}.` - : `Autosave is OFF${sandbox ?? 'for this sandbox page'}, and you haven't saved for ${elapsedTime} minutes.`; + ? `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; return Reminder... diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 83e91b1a2..f28d97777 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -210,8 +210,8 @@ const NewPage = (props)=>{ resetWarnUnsavedTimer(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ?? 'for this sandbox page'}.` - : `Autosave is OFF${sandbox ?? 'for this sandbox page'}, and you haven't saved for ${elapsedTime} minutes.`; + ? `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; return Reminder... From e4d66110274ce6132bd0c6b2ffe41354167bde35 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 21:39:51 +0000 Subject: [PATCH 09/37] Restore time to 15 minutes --- client/homebrew/pages/editPage/editPage.jsx | 6 +++--- client/homebrew/pages/homePage/homePage.jsx | 6 +++--- client/homebrew/pages/newPage/newPage.jsx | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 386f2afb0..5f369e7c5 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -40,7 +40,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 90000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; @@ -315,8 +315,8 @@ const EditPage = (props)=>{ resetWarnUnsavedTimer(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; + ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; return Reminder... diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 0c85fa48c..2e4137049 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -33,7 +33,7 @@ import Headtags from '@vitreum/headtags.js'; const Meta = Headtags.Meta; const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 90000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; @@ -160,8 +160,8 @@ const HomePage =(props)=>{ resetWarnUnsavedTimer(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; + ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; return Reminder... diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index f28d97777..f0c9ac323 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -29,7 +29,7 @@ const { both: RecentNavItem } = RecentNavItems; // Page specific imports const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 90000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; @@ -210,8 +210,8 @@ const NewPage = (props)=>{ resetWarnUnsavedTimer(); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? 'for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; + ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; return Reminder... From 82efff8ce52952d2d37fae2a3ad70e4386daebc6 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 20:10:17 -0400 Subject: [PATCH 10/37] Move setup effects to common functions --- client/homebrew/pages/editPage/editPage.jsx | 30 ------------------- client/homebrew/pages/homePage/homePage.jsx | 30 ------------------- client/homebrew/pages/newPage/newPage.jsx | 30 ------------------- .../homebrew/utils/commonEditPageFunctions.js | 29 ++++++++++++++++++ 4 files changed, 29 insertions(+), 90 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 5f369e7c5..f88b3877f 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -100,36 +100,6 @@ const EditPage = (props)=>{ hbfm }); - useEffect(()=>{ - const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); - - setAutoSaveEnabled(autoSavePref); - setWarnUnsavedChanges(!autoSavePref); - setHTMLErrors(hbfm.validate(currentBrew.text)); - fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - - const handleControlKeys = (e)=>{ - if(!(e.ctrlKey || e.metaKey)) return; - if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle); - if(e.keyCode === 80) printCurrentBrew(); - if([83, 80].includes(e.keyCode)) { - e.stopPropagation(); - e.preventDefault(); - } - }; - - document.addEventListener('keydown', handleControlKeys); - window.onbeforeunload = ()=>{ - if(unsavedChangesRef.current) - return 'You have unsaved changes!'; - }; - - return ()=>{ - document.removeEventListener('keydown', handleControlKeys); - window.onbeforeunload = null; - }; - }, []); - useEffect(()=>{ trySaveRef.current = trySave; unsavedChangesRef.current = unsavedChanges; diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 2e4137049..b15787d48 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -86,36 +86,6 @@ const HomePage =(props)=>{ hbfm }); - useEffect(()=>{ - const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); - - setAutoSaveEnabled(autoSavePref); - setWarnUnsavedChanges(!autoSavePref); - setHTMLErrors(hbfm.validate(currentBrew.text)); - fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - - const handleControlKeys = (e)=>{ - if(!(e.ctrlKey || e.metaKey)) return; - if(e.keyCode === 83) trySaveRef.current(true); - if(e.keyCode === 80) printCurrentBrew(); - if([83, 80].includes(e.keyCode)) { - e.stopPropagation(); - e.preventDefault(); - } - }; - - document.addEventListener('keydown', handleControlKeys); - window.onbeforeunload = ()=>{ - if(unsavedChangesRef.current) - return 'You have unsaved changes!'; - }; - - return ()=>{ - document.removeEventListener('keydown', handleControlKeys); - window.onbeforeunload = null; - }; - }, []); - useEffect(()=>{ unsavedChangesRef.current = unsavedChanges; }, [unsavedChanges]); diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index f0c9ac323..2b3c9e4d3 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -91,36 +91,6 @@ const NewPage = (props)=>{ hbfm }); - useEffect(()=>{ - const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); - - setAutoSaveEnabled(autoSavePref); - setWarnUnsavedChanges(!autoSavePref); - setHTMLErrors(hbfm.validate(currentBrew.text)); - fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - - const handleControlKeys = (e)=>{ - if(!(e.ctrlKey || e.metaKey)) return; - if(e.keyCode === 83) trySaveRef.current(true); - if(e.keyCode === 80) printCurrentBrew(); - if([83, 80].includes(e.keyCode)) { - e.stopPropagation(); - e.preventDefault(); - } - }; - - document.addEventListener('keydown', handleControlKeys); - window.onbeforeunload = ()=>{ - if(unsavedChangesRef.current) - return 'You have unsaved changes!'; - }; - - return ()=>{ - document.removeEventListener('keydown', handleControlKeys); - window.onbeforeunload = null; - }; - }, []); - const loadBrew = ()=>{ const brew = { ...currentBrew }; if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index bf397e334..4536cc1c5 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -14,6 +14,35 @@ export default function useCommonEditPageFunctions(dependencies) { hbfm } = dependencies; + //==--------- Page setup ----------==// + useEffect(()=>{ + const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); + setAutoSaveEnabled(autoSavePref); + setWarnUnsavedChanges(!autoSavePref); + setHTMLErrors(hbfm.validate(currentBrew.text)); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); + + const handleControlKeys = (e)=>{ + if(!(e.ctrlKey || e.metaKey)) return; + if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle); + if(e.keyCode === 80) printCurrentBrew(); + if([83, 80].includes(e.keyCode)) { + e.stopPropagation(); + e.preventDefault(); + } + }; + + document.addEventListener('keydown', handleControlKeys); + window.onbeforeunload = ()=>{ + if(unsavedChangesRef.current) + return 'You have unsaved changes!'; + }; + return ()=>{ + document.removeEventListener('keydown', handleControlKeys); + window.onBeforeUnload = null; + }; + }, []); + const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' if(subfield == 'renderer' || subfield == 'theme') fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); From c3a2d0641a33b3db594fa95de43bea650517b252 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 20:23:30 -0400 Subject: [PATCH 11/37] Fix setAlertOwnershipToTransfer typo --- client/homebrew/pages/editPage/editPage.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 5f369e7c5..30cf67153 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -161,7 +161,7 @@ const EditPage = (props)=>{ const handleGoogleClick = ()=>{ if(global.account !== currentBrew.authors[0]) { - setalertOwnershipToTransfer(true); + setAlertOwnershipToTransfer(true); return; } if(!global.account?.googleId) { @@ -178,7 +178,7 @@ const EditPage = (props)=>{ setAlertTrashedGoogleBrew(false); setAlertNoGoogleToTransfer(false); setConfirmGoogleTransfer(false); - setalertOwnershipToTransfer(false); + setAlertOwnershipToTransfer(false); }; const toggleGoogleStorage = (e)=>{ From 4a884233c19915446e8c0f650d172c03cb75307c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 20:29:39 -0400 Subject: [PATCH 12/37] Remove page leave warning when saving on New --- client/homebrew/pages/newPage/newPage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index f0c9ac323..ff686e0a1 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -197,6 +197,7 @@ const NewPage = (props)=>{ localStorage.removeItem(BREWKEY); localStorage.removeItem(STYLEKEY); localStorage.removeItem(METAKEY); + window.onbeforeunload = null; window.location = `/edit/${savedBrew.editId}`; }; From da882d6f006c5961bf7a1f102b19b1b6261e57aa Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Wed, 9 Sep 2026 20:55:30 -0400 Subject: [PATCH 13/37] Fix google transfer ownership popup priority Only show alertOwnershipToTransfer if: 1) current user is logged in 2) brew actually has authors 3) current user is not author[0] In all other cases, it should prompt to log in with a google account instead. --- client/homebrew/pages/editPage/editPage.jsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 30cf67153..df5e8e592 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -160,7 +160,7 @@ const EditPage = (props)=>{ }; const handleGoogleClick = ()=>{ - if(global.account !== currentBrew.authors[0]) { + if(currentBrew.authors.length > 0 && global.account?.username !== currentBrew.authors[0]) { setAlertOwnershipToTransfer(true); return; } @@ -264,12 +264,11 @@ const EditPage = (props)=>{ Google Drive icon - - {alertOwnershipToTransfer && (
- You must be the Owner to transfer between the Homebrewery and Google Drive! - The owner of this file is {currentBrew.authors[0]} + You must be the Owner to transfer between the Homebrewery and Google Drive! + The owner of this file is {currentBrew.authors[0]}. +

Okay
)} From e6bf0e28c5bcf8288aaab348272a35162949c12a Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 11 Sep 2026 22:28:19 -0400 Subject: [PATCH 14/37] Pass in the remaining missing dependencies --- client/homebrew/pages/editPage/editPage.jsx | 13 +++++++++---- client/homebrew/pages/homePage/homePage.jsx | 13 +++++++++---- client/homebrew/pages/newPage/newPage.jsx | 11 ++++++++--- .../homebrew/utils/commonEditPageFunctions.js | 19 ++++++++++++++++--- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index f8c99f531..088f13765 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -8,7 +8,6 @@ import { hbfm } from 'hbmarkedwrapper'; import _ from 'lodash'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; -import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' @@ -43,7 +42,6 @@ const SAVE_TIMEOUT = 10000; const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds -const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -90,14 +88,21 @@ const EditPage = (props)=>{ setThemeBundle, HTMLErrors, setHTMLErrors, + currentBrew, setCurrentBrew, useLocalStorage, BREWKEY, STYLEKEY, SNIPKEY, METAKEY, - fetchThemeBundle, - hbfm + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + trySaveRef, + unsavedChangesRef, + sandbox, + saveGoogle }); useEffect(()=>{ diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index b15787d48..29e5b70e2 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -8,7 +8,6 @@ import { hbfm } from 'hbmarkedwrapper'; import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; -import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' @@ -36,7 +35,6 @@ const SAVE_TIMEOUT = 10000; const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds -const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -67,6 +65,7 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const warnUnsavedTimeout = useRef(null); + const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); const { @@ -76,14 +75,20 @@ const HomePage =(props)=>{ setThemeBundle, HTMLErrors, setHTMLErrors, + currentBrew, setCurrentBrew, useLocalStorage, BREWKEY, STYLEKEY, SNIPKEY, METAKEY, - fetchThemeBundle, - hbfm + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + trySaveRef, + unsavedChangesRef, + sandbox }); useEffect(()=>{ diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index e4f30e7d5..3208af496 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -32,7 +32,6 @@ const SAVE_TIMEOUT = 10000; const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds -const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -81,14 +80,20 @@ const NewPage = (props)=>{ setThemeBundle, HTMLErrors, setHTMLErrors, + currentBrew, setCurrentBrew, useLocalStorage, BREWKEY, STYLEKEY, SNIPKEY, METAKEY, - fetchThemeBundle, - hbfm + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + trySaveRef, + unsavedChangesRef, + sandbox }); const loadBrew = ()=>{ diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index 4536cc1c5..10327bf2a 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -1,23 +1,36 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; + +const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; + export default function useCommonEditPageFunctions(dependencies) { const { setError, setThemeBundle, HTMLErrors, setHTMLErrors, + currentBrew, setCurrentBrew, useLocalStorage, BREWKEY, STYLEKEY, SNIPKEY, METAKEY, - fetchThemeBundle, - hbfm + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + trySaveRef, + sandbox, + saveGoogle = false, + unsavedChangesRef } = dependencies; //==--------- Page setup ----------==// useEffect(()=>{ - const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); + const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); setAutoSaveEnabled(autoSavePref); + console.log(autoSavePref) setWarnUnsavedChanges(!autoSavePref); setHTMLErrors(hbfm.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); From bfb119e62dfabd1a4d177536bb245160a592546d Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 11 Sep 2026 22:37:46 -0400 Subject: [PATCH 15/37] Spaces to tabs --- .../homebrew/utils/commonEditPageFunctions.js | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index 10327bf2a..8f41ce421 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -4,33 +4,33 @@ import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; export default function useCommonEditPageFunctions(dependencies) { - const { - setError, - setThemeBundle, - HTMLErrors, - setHTMLErrors, - currentBrew, - setCurrentBrew, - useLocalStorage, - BREWKEY, - STYLEKEY, - SNIPKEY, - METAKEY, - hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - trySaveRef, - sandbox, - saveGoogle = false, - unsavedChangesRef - } = dependencies; + const { + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + currentBrew, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + trySaveRef, + sandbox, + saveGoogle = false, + unsavedChangesRef + } = dependencies; - //==--------- Page setup ----------==// - useEffect(()=>{ + //==--------- Page setup ----------==// + useEffect(()=>{ const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); setAutoSaveEnabled(autoSavePref); - console.log(autoSavePref) + console.log(autoSavePref) setWarnUnsavedChanges(!autoSavePref); setHTMLErrors(hbfm.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -56,30 +56,30 @@ export default function useCommonEditPageFunctions(dependencies) { }; }, []); - const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' - if(subfield == 'renderer' || subfield == 'theme') - fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); + const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' + if(subfield == 'renderer' || subfield == 'theme') + fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); - //If there are HTML errors, run the validator on every change to give quick feedback - if(HTMLErrors.length && (field == 'text' || field == 'snippets')) - setHTMLErrors(hbfm.validate(value)); + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length && (field == 'text' || field == 'snippets')) + setHTMLErrors(hbfm.validate(value)); - if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); - else setCurrentBrew((prev)=>({ ...prev, [field]: value })); + if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value })); + else setCurrentBrew((prev)=>({ ...prev, [field]: value })); - if(useLocalStorage) { - if(field == 'text') localStorage.setItem(BREWKEY, value); - if(field == 'style') localStorage.setItem(STYLEKEY, value); - if(field == 'snippets') localStorage.setItem(SNIPKEY, value); - if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ - renderer : value.renderer, - theme : value.theme, - lang : value.lang - })); - } - }; + if(useLocalStorage) { + if(field == 'text') localStorage.setItem(BREWKEY, value); + if(field == 'style') localStorage.setItem(STYLEKEY, value); + if(field == 'snippets') localStorage.setItem(SNIPKEY, value); + if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ + renderer : value.renderer, + theme : value.theme, + lang : value.lang + })); + } + }; - return { - handleBrewChange - } + return { + handleBrewChange + } } \ No newline at end of file From b9ae2cd16cf9440250d7b1fd1bc07d2a65849eb9 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 11 Sep 2026 23:21:21 -0400 Subject: [PATCH 16/37] Move "unsaved changes" check to common hook --- client/homebrew/pages/editPage/editPage.jsx | 11 +++-------- client/homebrew/pages/homePage/homePage.jsx | 11 +++-------- client/homebrew/pages/newPage/newPage.jsx | 11 +++-------- client/homebrew/utils/commonEditPageFunctions.js | 14 +++++++++++++- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 088f13765..3875b3d04 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -84,6 +84,7 @@ const EditPage = (props)=>{ const { handleBrewChange } = useCommonEditPageFunctions({ + saveGoogle, setError, setThemeBundle, HTMLErrors, @@ -101,8 +102,9 @@ const EditPage = (props)=>{ setWarnUnsavedChanges, trySaveRef, unsavedChangesRef, + setUnsavedChanges, sandbox, - saveGoogle + lastSavedBrew }); useEffect(()=>{ @@ -110,13 +112,6 @@ const EditPage = (props)=>{ unsavedChangesRef.current = unsavedChanges; }); - useEffect(()=>{ - const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); - setUnsavedChanges(hasChange); - - if(autoSaveEnabled) trySave(false, hasChange, saveGoogle); - }, [currentBrew]); - const handleSplitMove = ()=>{ editorRef.current?.update(); }; diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 29e5b70e2..278ec6eb3 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -88,7 +88,9 @@ const HomePage =(props)=>{ setWarnUnsavedChanges, trySaveRef, unsavedChangesRef, - sandbox + setUnsavedChanges, + sandbox, + lastSavedBrew }); useEffect(()=>{ @@ -108,13 +110,6 @@ const HomePage =(props)=>{ }); }; - useEffect(()=>{ - const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); - setUnsavedChanges(hasChange); - - if(autoSaveEnabled) trySave(false, hasChange); - }, [currentBrew]); - const handleSplitMove = ()=>{ editorRef.current.update(); }; diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 3208af496..fed59493e 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -93,7 +93,9 @@ const NewPage = (props)=>{ setWarnUnsavedChanges, trySaveRef, unsavedChangesRef, - sandbox + setUnsavedChanges, + sandbox, + lastSavedBrew }); const loadBrew = ()=>{ @@ -125,13 +127,6 @@ const NewPage = (props)=>{ window.history.replaceState({}, window.location.title, '/new/'); }; - useEffect(()=>{ - const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); - setUnsavedChanges(hasChange); - - if(autoSaveEnabled) trySave(false, hasChange); - }, [currentBrew]); - useEffect(()=>{ trySaveRef.current = trySave; unsavedChangesRef.current = unsavedChanges; diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index 8f41ce421..df6185506 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -1,5 +1,6 @@ import React, { useState, useEffect, useRef } from 'react'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; +import _ from 'lodash'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; @@ -23,7 +24,9 @@ export default function useCommonEditPageFunctions(dependencies) { trySaveRef, sandbox, saveGoogle = false, - unsavedChangesRef + unsavedChangesRef, + setUnsavedChanges, + lastSavedBrew } = dependencies; //==--------- Page setup ----------==// @@ -56,6 +59,15 @@ export default function useCommonEditPageFunctions(dependencies) { }; }, []); + //======----- Check for unsaved changes and autosave if enabled -----====== + useEffect(()=>{ + const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); + setUnsavedChanges(hasChange); + + if(autoSaveEnabled) trySaveRef.current(false, hasChange, saveGoogle); + }, [currentBrew]); + + const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' if(subfield == 'renderer' || subfield == 'theme') fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); From e2cec264a657f69eca2d44edccf618c26f8cfaf2 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 12 Sep 2026 12:08:51 -0400 Subject: [PATCH 17/37] Convert trySaveRef to useEffectEvent --- client/homebrew/pages/editPage/editPage.jsx | 60 +++++++++---------- client/homebrew/pages/homePage/homePage.jsx | 52 ++++++++-------- client/homebrew/pages/newPage/newPage.jsx | 58 +++++++++--------- .../homebrew/utils/commonEditPageFunctions.js | 6 +- 4 files changed, 85 insertions(+), 91 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 3875b3d04..75043aa44 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -2,7 +2,7 @@ import './editPage.less'; // Common imports -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useEffectEvent } from 'react'; import request from '../../utils/request-middleware.js'; import { hbfm } from 'hbmarkedwrapper'; import _ from 'lodash'; @@ -78,37 +78,9 @@ const EditPage = (props)=>{ const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const saveTimeout = useRef(null); const warnUnsavedTimeout = useRef(null); - const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges - const { - handleBrewChange - } = useCommonEditPageFunctions({ - saveGoogle, - setError, - setThemeBundle, - HTMLErrors, - setHTMLErrors, - currentBrew, - setCurrentBrew, - useLocalStorage, - BREWKEY, - STYLEKEY, - SNIPKEY, - METAKEY, - hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - trySaveRef, - unsavedChangesRef, - setUnsavedChanges, - sandbox, - lastSavedBrew - }); - useEffect(()=>{ - trySaveRef.current = trySave; unsavedChangesRef.current = unsavedChanges; }); @@ -159,7 +131,7 @@ const EditPage = (props)=>{ trySave(true, true, newSaveGoogle); }; - const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{ + const trySave = useEffectEvent((immediate = false, hasChanges = true, saveToGoogle = false)=>{ clearTimeout(saveTimeout.current); if(isSaving) return; if(!hasChanges && !immediate) return; @@ -176,7 +148,7 @@ const EditPage = (props)=>{ setLastSavedTime(new Date()); if(!autoSaveEnabled) resetWarnUnsavedTimer(); }, newTimeout); - }; + }); const save = async (brew, saveToGoogle)=>{ setHTMLErrors(hbfm.validate(brew.text)); @@ -353,6 +325,32 @@ const EditPage = (props)=>{ ; }; + const { + handleBrewChange + } = useCommonEditPageFunctions({ + saveGoogle, + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + currentBrew, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + unsavedChangesRef, + setUnsavedChanges, + trySave, + sandbox, + lastSavedBrew + }); + return (
diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 278ec6eb3..2c0de0e0a 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -2,7 +2,7 @@ import './homePage.less'; // Common imports -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useEffectEvent } from 'react'; import request from '../../utils/request-middleware.js'; import { hbfm } from 'hbmarkedwrapper'; import _ from 'lodash'; @@ -65,34 +65,8 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const warnUnsavedTimeout = useRef(null); - const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); - const { - handleBrewChange - } = useCommonEditPageFunctions({ - setError, - setThemeBundle, - HTMLErrors, - setHTMLErrors, - currentBrew, - setCurrentBrew, - useLocalStorage, - BREWKEY, - STYLEKEY, - SNIPKEY, - METAKEY, - hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - trySaveRef, - unsavedChangesRef, - setUnsavedChanges, - sandbox, - lastSavedBrew - }); - useEffect(()=>{ unsavedChangesRef.current = unsavedChanges; }, [unsavedChanges]); @@ -176,6 +150,30 @@ const HomePage =(props)=>{ ; }; + const { + handleBrewChange + } = useCommonEditPageFunctions({ + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + currentBrew, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + unsavedChangesRef, + setUnsavedChanges, + sandbox, + lastSavedBrew + }); + return (
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index fed59493e..c4f21c982 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -2,7 +2,7 @@ import './newPage.less'; // Common imports -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useEffectEvent } from 'react'; import request from '../../utils/request-middleware.js'; import { hbfm } from 'hbmarkedwrapper'; import _ from 'lodash'; @@ -66,38 +66,12 @@ const NewPage = (props)=>{ const lastSavedBrew = useRef(_.cloneDeep(props.brew)); // const saveTimeout = useRef(null); const warnUnsavedTimeout = useRef(null); - const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges useEffect(()=>{ loadBrew(); }, []); - const { - handleBrewChange - } = useCommonEditPageFunctions({ - setError, - setThemeBundle, - HTMLErrors, - setHTMLErrors, - currentBrew, - setCurrentBrew, - useLocalStorage, - BREWKEY, - STYLEKEY, - SNIPKEY, - METAKEY, - hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - trySaveRef, - unsavedChangesRef, - setUnsavedChanges, - sandbox, - lastSavedBrew - }); - const loadBrew = ()=>{ const brew = { ...currentBrew }; if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser @@ -128,7 +102,6 @@ const NewPage = (props)=>{ }; useEffect(()=>{ - trySaveRef.current = trySave; unsavedChangesRef.current = unsavedChanges; }); @@ -142,7 +115,7 @@ const NewPage = (props)=>{ warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings }; - const trySave = async ()=>{ + const trySave = useEffectEvent(async ()=>{ setIsSaving(true); const updatedBrew = { ...currentBrew }; @@ -169,7 +142,7 @@ const NewPage = (props)=>{ localStorage.removeItem(METAKEY); window.onbeforeunload = null; window.location = `/edit/${savedBrew.editId}`; - }; + }); const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING @@ -231,6 +204,31 @@ const NewPage = (props)=>{ ); + const { + handleBrewChange + } = useCommonEditPageFunctions({ + setError, + setThemeBundle, + HTMLErrors, + setHTMLErrors, + currentBrew, + setCurrentBrew, + useLocalStorage, + BREWKEY, + STYLEKEY, + SNIPKEY, + METAKEY, + hbfm, + autoSaveEnabled, + setAutoSaveEnabled, + setWarnUnsavedChanges, + unsavedChangesRef, + setUnsavedChanges, + trySave, + sandbox, + lastSavedBrew + }); + return (
{renderNavbar()} diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index df6185506..96368d15b 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -21,7 +21,7 @@ export default function useCommonEditPageFunctions(dependencies) { autoSaveEnabled, setAutoSaveEnabled, setWarnUnsavedChanges, - trySaveRef, + trySave = ()=>{}, sandbox, saveGoogle = false, unsavedChangesRef, @@ -40,7 +40,7 @@ export default function useCommonEditPageFunctions(dependencies) { const handleControlKeys = (e)=>{ if(!(e.ctrlKey || e.metaKey)) return; - if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle); + if(e.keyCode === 83) trySave(true, true, saveGoogle); if(e.keyCode === 80) printCurrentBrew(); if([83, 80].includes(e.keyCode)) { e.stopPropagation(); @@ -64,7 +64,7 @@ export default function useCommonEditPageFunctions(dependencies) { const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); setUnsavedChanges(hasChange); - if(autoSaveEnabled) trySaveRef.current(false, hasChange, saveGoogle); + if(autoSaveEnabled) trySave(false, hasChange, saveGoogle); }, [currentBrew]); From b26d81ca08c85815860e5fd4125daf7e116e8ce0 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 12 Sep 2026 12:30:21 -0400 Subject: [PATCH 18/37] Move UnsavedChangesRef into common hook and clean syncing useEffect --- client/homebrew/pages/editPage/editPage.jsx | 7 +------ client/homebrew/pages/homePage/homePage.jsx | 7 +------ client/homebrew/pages/newPage/newPage.jsx | 7 +------ client/homebrew/utils/commonEditPageFunctions.js | 5 ++++- 4 files changed, 7 insertions(+), 19 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 75043aa44..003fc64ec 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -78,11 +78,6 @@ const EditPage = (props)=>{ const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const saveTimeout = useRef(null); const warnUnsavedTimeout = useRef(null); - const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges - - useEffect(()=>{ - unsavedChangesRef.current = unsavedChanges; - }); const handleSplitMove = ()=>{ editorRef.current?.update(); @@ -344,7 +339,7 @@ const EditPage = (props)=>{ autoSaveEnabled, setAutoSaveEnabled, setWarnUnsavedChanges, - unsavedChangesRef, + unsavedChanges, setUnsavedChanges, trySave, sandbox, diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 2c0de0e0a..1adb77e9a 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -65,11 +65,6 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const warnUnsavedTimeout = useRef(null); - const unsavedChangesRef = useRef(unsavedChanges); - - useEffect(()=>{ - unsavedChangesRef.current = unsavedChanges; - }, [unsavedChanges]); const save = ()=>{ request.post('/api') @@ -168,7 +163,7 @@ const HomePage =(props)=>{ autoSaveEnabled, setAutoSaveEnabled, setWarnUnsavedChanges, - unsavedChangesRef, + unsavedChanges, setUnsavedChanges, sandbox, lastSavedBrew diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index c4f21c982..ea15199c8 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -66,7 +66,6 @@ const NewPage = (props)=>{ const lastSavedBrew = useRef(_.cloneDeep(props.brew)); // const saveTimeout = useRef(null); const warnUnsavedTimeout = useRef(null); - const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges useEffect(()=>{ loadBrew(); @@ -101,10 +100,6 @@ const NewPage = (props)=>{ window.history.replaceState({}, window.location.title, '/new/'); }; - useEffect(()=>{ - unsavedChangesRef.current = unsavedChanges; - }); - const handleSplitMove = ()=>{ editorRef.current.update(); }; @@ -222,7 +217,7 @@ const NewPage = (props)=>{ autoSaveEnabled, setAutoSaveEnabled, setWarnUnsavedChanges, - unsavedChangesRef, + unsavedChanges, setUnsavedChanges, trySave, sandbox, diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index 96368d15b..4a75feaf8 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -24,11 +24,13 @@ export default function useCommonEditPageFunctions(dependencies) { trySave = ()=>{}, sandbox, saveGoogle = false, - unsavedChangesRef, + unsavedChanges, setUnsavedChanges, lastSavedBrew } = dependencies; + const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges + //==--------- Page setup ----------==// useEffect(()=>{ const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); @@ -63,6 +65,7 @@ export default function useCommonEditPageFunctions(dependencies) { useEffect(()=>{ const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); setUnsavedChanges(hasChange); + unsavedChangesRef.current = hasChange; if(autoSaveEnabled) trySave(false, hasChange, saveGoogle); }, [currentBrew]); From 9441582aa379915a68ba619e128356e076701e24 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 12 Sep 2026 12:50:27 -0400 Subject: [PATCH 19/37] Move handleSplitMove to common hook --- client/homebrew/pages/editPage/editPage.jsx | 8 +++----- client/homebrew/pages/homePage/homePage.jsx | 14 ++++++-------- client/homebrew/pages/newPage/newPage.jsx | 14 ++++++-------- client/homebrew/utils/commonEditPageFunctions.js | 8 ++++++-- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 003fc64ec..dea5606f2 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -79,10 +79,6 @@ const EditPage = (props)=>{ const saveTimeout = useRef(null); const warnUnsavedTimeout = useRef(null); - const handleSplitMove = ()=>{ - editorRef.current?.update(); - }; - const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ ...prevBrew, style : newData.style, @@ -321,6 +317,7 @@ const EditPage = (props)=>{ }; const { + handleSplitMove, handleBrewChange } = useCommonEditPageFunctions({ saveGoogle, @@ -343,7 +340,8 @@ const EditPage = (props)=>{ setUnsavedChanges, trySave, sandbox, - lastSavedBrew + lastSavedBrew, + editorRef }); return ( diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 1adb77e9a..1e02d67b8 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -62,8 +62,8 @@ const HomePage =(props)=>{ const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); - const editorRef = useRef(null); - const lastSavedBrew = useRef(_.cloneDeep(props.brew)); + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const warnUnsavedTimeout = useRef(null); const save = ()=>{ @@ -79,10 +79,6 @@ const HomePage =(props)=>{ }); }; - const handleSplitMove = ()=>{ - editorRef.current.update(); - }; - const resetWarnUnsavedTimer = ()=>{ setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds clearTimeout(warnUnsavedTimeout.current); @@ -145,7 +141,8 @@ const HomePage =(props)=>{ ; }; - const { + const { + handleSplitMove, handleBrewChange } = useCommonEditPageFunctions({ setError, @@ -166,7 +163,8 @@ const HomePage =(props)=>{ unsavedChanges, setUnsavedChanges, sandbox, - lastSavedBrew + lastSavedBrew, + editorRef }); return ( diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index ea15199c8..8182ac20c 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -62,8 +62,8 @@ const NewPage = (props)=>{ const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); - const editorRef = useRef(null); - const lastSavedBrew = useRef(_.cloneDeep(props.brew)); + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); // const saveTimeout = useRef(null); const warnUnsavedTimeout = useRef(null); @@ -100,10 +100,6 @@ const NewPage = (props)=>{ window.history.replaceState({}, window.location.title, '/new/'); }; - const handleSplitMove = ()=>{ - editorRef.current.update(); - }; - const resetWarnUnsavedTimer = ()=>{ setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds clearTimeout(warnUnsavedTimeout.current); @@ -199,7 +195,8 @@ const NewPage = (props)=>{ ); - const { + const { + handleSplitMove, handleBrewChange } = useCommonEditPageFunctions({ setError, @@ -221,7 +218,8 @@ const NewPage = (props)=>{ setUnsavedChanges, trySave, sandbox, - lastSavedBrew + lastSavedBrew, + editorRef }); return ( diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index 4a75feaf8..ff53981f8 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -26,7 +26,8 @@ export default function useCommonEditPageFunctions(dependencies) { saveGoogle = false, unsavedChanges, setUnsavedChanges, - lastSavedBrew + lastSavedBrew, + editorRef } = dependencies; const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges @@ -35,7 +36,6 @@ export default function useCommonEditPageFunctions(dependencies) { useEffect(()=>{ const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); setAutoSaveEnabled(autoSavePref); - console.log(autoSavePref) setWarnUnsavedChanges(!autoSavePref); setHTMLErrors(hbfm.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -70,6 +70,9 @@ export default function useCommonEditPageFunctions(dependencies) { if(autoSaveEnabled) trySave(false, hasChange, saveGoogle); }, [currentBrew]); + const handleSplitMove = ()=>{ + editorRef.current.update(); + }; const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' if(subfield == 'renderer' || subfield == 'theme') @@ -95,6 +98,7 @@ export default function useCommonEditPageFunctions(dependencies) { }; return { + handleSplitMove, handleBrewChange } } \ No newline at end of file From 01a809a5c7f6f87408bcdf923b8ec65764473ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 10:39:41 +0200 Subject: [PATCH 20/37] split pages into their own routes file --- server/app.js | 361 ++-------------------------------------- server/page-routes.js | 377 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 346 deletions(-) create mode 100644 server/page-routes.js diff --git a/server/app.js b/server/app.js index b1e0e1c25..9e04c7b5b 100644 --- a/server/app.js +++ b/server/app.js @@ -16,18 +16,15 @@ import path from 'path'; import fs from 'fs-extra'; import api from './homebrew.api.js'; +import pageRoutes from './page-routes.js'; const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api; import adminApi from './admin.api.js'; import vaultApi from './vault.api.js'; import GoogleActions from './googleActions.js'; import serveCompressedStaticAssets from './static-assets.mv.js'; -import sanitizeFilename from 'sanitize-filename'; import asyncHandler from 'express-async-handler'; import { model as HomebrewModel } from './homebrew.model.js'; -import { DEFAULT_BREW } from './brewDefaults.js'; -import { splitTextStyleAndMetadata } from '../shared/helpers.js'; - //==== Middleware Imports ====// import contentNegotiation from './middleware/content-negotiation.js'; import bodyParser from 'body-parser'; @@ -132,134 +129,25 @@ export default async function createApp(vite) { type : 'website' }; + app.use(pageRoutes({ + defaultMetaTags, + welcomeText, + welcomeTextLegacy, + migrateText, + changelogText, + faqText, + getBrew, + getUsersBrewThemes, + HomebrewModel, + GoogleActions, + sanitizeBrew, + })); + //Robots.txt app.get('/robots.txt', (req, res)=>{ return res.sendFile(`robots.txt`, { root: process.cwd() }); }); - //Home page - app.get('/', (req, res, next)=>{ - req.brew = { - text : welcomeText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'Homepage', - description : 'Homepage' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Home page Legacy - app.get('/legacy', (req, res, next)=>{ - req.brew = { - text : welcomeTextLegacy, - renderer : 'legacy', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'Homepage (Legacy)', - description : 'Homepage' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Legacy/Other Document -> v3 Migration Guide - app.get('/migrate', (req, res, next)=>{ - req.brew = { - text : migrateText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'v3 Migration Guide', - description : 'A brief guide to converting Legacy documents to the v3 renderer.' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Changelog page - app.get('/changelog', async (req, res, next)=>{ - req.brew = { - title : 'Changelog', - text : changelogText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'Changelog', - description : 'Development changelog.' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //FAQ page - app.get('/faq', async (req, res, next)=>{ - req.brew = { - title : 'FAQ', - text : faqText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'FAQ', - description : 'Frequently Asked Questions' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Source page - app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{ - const { brew } = req; - - const replaceStrings = { '&': '&', '<': '<', '>': '>' }; - let text = brew.text; - for (const replaceStr in replaceStrings) { - text = text.replaceAll(replaceStr, replaceStrings[replaceStr]); - } - text = `
${text}
`; - res.status(200).send(text); - }); - - //Download brew source page - app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{ - const { brew } = req; - sanitizeBrew(brew, 'share'); - const prefix = 'HB - '; - - const encodeRFC3986ValueChars = (str)=>{ - return ( - encodeURIComponent(str) - .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;}) - ); - }; - - let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); - if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; - res.set({ - 'Cache-Control' : 'no-cache', - 'Content-Type' : 'text/plain', - 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt` - }); - res.status(200).send(brew.text); - }); - //Serve brew metadata app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{ const { brew } = req; @@ -280,78 +168,6 @@ export default async function createApp(vite) { //Serve brew styling app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);}); - //User Page - app.get('/user/:username', dbCheck, async (req, res, next)=>{ - const ownAccount = req.account && (req.account.username == req.params.username); - - req.ogMeta = { ...defaultMetaTags, - title : `${req.params.username}'s Collection`, - description : 'View my collection of homebrew on the Homebrewery.' - // type : could be 'profile'? - }; - - const fields = [ - 'googleId', - 'title', - 'pageCount', - 'description', - 'authors', - 'lang', - 'published', - 'views', - 'shareId', - 'editId', - 'createdAt', - 'updatedAt', - 'lastViewed', - 'thumbnail', - 'tags' - ]; - - let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields) - .catch((err)=>{ - console.log(err); - }); - - brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed" - - if(ownAccount && req?.account?.googleId){ - const auth = await GoogleActions.authCheck(req.account, res); - let googleBrews = await GoogleActions.listGoogleBrews(auth) - .catch((err)=>{ - console.error(err); - }); - - // If stub matches file from Google, use Google metadata over stub metadata - if(googleBrews && googleBrews.length > 0) { - for (const brew of brews.filter((brew)=>brew.googleId)) { - const match = googleBrews.findIndex((b)=>b.editId === brew.editId); - if(match !== -1) { - brew.googleId = googleBrews[match].googleId; - brew.pageCount = googleBrews[match].pageCount; - brew.renderer = googleBrews[match].renderer; - brew.version = googleBrews[match].version; - brew.webViewLink = googleBrews[match].webViewLink; - googleBrews.splice(match, 1); - } - } - - //Remaining unstubbed google brews display current user as author - googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] })); - brews = _.concat(brews, googleBrews); - } - } - - req.brews = _.map(brews, (brew)=>{ - // Clean up brew data - brew.title = brew.title?.trim(); - brew.description = brew.description?.trim(); - return sanitizeBrew(brew, ownAccount ? 'edit' : 'share'); - }); - - return next(); - }); - //Change author name on brews app.put('/api/user/rename', dbCheck, async (req, res)=>{ const { username, newUsername } = req.body; @@ -380,144 +196,6 @@ export default async function createApp(vite) { } }); - //Edit Page - app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{ - req.brew = req.brew.toObject ? req.brew.toObject() : req.brew; - - req.userThemes = await(getUsersBrewThemes(req.account?.username)); - - req.ogMeta = { ...defaultMetaTags, - title : req.brew.title || 'Untitled Brew', - description : req.brew.description || 'No description.', - image : req.brew.thumbnail || defaultMetaTags.image, - locale : req.brew.lang, - type : 'article' - }; - - sanitizeBrew(req.brew, 'edit'); - splitTextStyleAndMetadata(req.brew); - res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save. - return next(); - })); - - //New Page from ID - app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{ - sanitizeBrew(req.brew, 'share'); - splitTextStyleAndMetadata(req.brew); - const brew = { - shareId : req.brew.shareId, - title : `CLONE - ${req.brew.title}`, - text : req.brew.text, - style : req.brew.style, - renderer : req.brew.renderer, - theme : req.brew.theme, - tags : req.brew.tags, - snippets : req.brew.snippets - }; - req.brew = _.defaults(brew, DEFAULT_BREW); - - req.userThemes = await(getUsersBrewThemes(req.account?.username)); - - req.ogMeta = { ...defaultMetaTags, - title : 'New', - description : 'Start crafting your homebrew on the Homebrewery!' - }; - - return next(); - })); - - //New Page - app.get('/new', asyncHandler(async(req, res, next)=>{ - req.userThemes = await(getUsersBrewThemes(req.account?.username)); - - req.ogMeta = { ...defaultMetaTags, - title : 'New', - description : 'Start crafting your homebrew on the Homebrewery!' - }; - - return next(); - })); - - //Share Page - app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ - const { brew } = req; - req.ogMeta = { ...defaultMetaTags, - title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, - description : req.brew.description || 'No description.', - image : req.brew.thumbnail || defaultMetaTags.image, - type : 'article' - }; - - // increase visitor view count, do not include visits by author(s) - if(!brew.authors.includes(req.account?.username)){ - if(req.params.id.length > 12 && !brew._id) { - const googleId = brew.googleId; - const shareId = brew.shareId; - await GoogleActions.increaseView(googleId, shareId, 'share', brew) - .catch((err)=>{next(err);}); - } else { - await HomebrewModel.increaseView({ shareId: brew.shareId }); - } - }; - - brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share'); - splitTextStyleAndMetadata(req.brew); - return next(); - })); - - //Account Page - app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ - const data = {}; - data.title = 'Account Information Page'; - - if(!req.account) { - res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"'); - const error = new Error('No valid account'); - error.status = 401; - error.HBErrorCode = '50'; - error.page = data.title; - return next(error); - }; - - let auth; - let googleCount = []; - if(req.account) { - if(req.account.googleId) { - auth = await GoogleActions.authCheck(req.account, res, false); - - googleCount = await GoogleActions.listGoogleBrews(auth) - .catch((err)=>{ - console.error(err); - }); - } - - const query = { authors: req.account.username, googleId: { $exists: false } }; - const mongoCount = await HomebrewModel.countDocuments(query) - .catch((err)=>{ - console.log(err); - return 0; - }); - - data.accountDetails = { - username : req.account.username, - issued : req.account.issued, - googleId : Boolean(req.account.googleId), - authCheck : Boolean(req.account.googleId && auth?.credentials.access_token), - mongoCount : mongoCount, - googleCount : googleCount?.length - }; - } - - req.brew = data; - - req.ogMeta = { ...defaultMetaTags, - title : `Account Page`, - description : null - }; - - return next(); - })); - // Local only if(isLocalEnvironment){ // Login @@ -534,15 +212,6 @@ export default async function createApp(vite) { app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages')); app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts')); - //Vault Page - app.get('/vault', asyncHandler(async(req, res, next)=>{ - req.ogMeta = { ...defaultMetaTags, - title : 'The Vault', - description : 'Search for Brews' - }; - return next(); - })); - //Send rendered page app.use(asyncHandler(async (req, res, next)=>{ if(!req.route) return res.redirect('/'); // Catch-all for invalid routes diff --git a/server/page-routes.js b/server/page-routes.js new file mode 100644 index 000000000..df9fc2c44 --- /dev/null +++ b/server/page-routes.js @@ -0,0 +1,377 @@ +/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ +// page-routes.js + +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +const __dirname = dirname(fileURLToPath(import.meta.url)); +process.chdir(`${__dirname}/..`); + +import _ from 'lodash'; +import express from 'express'; +import asyncHandler from 'express-async-handler'; + +//==== Middleware Imports ====// +import dbCheck from './middleware/dbCheck.js'; +import sanitizeFilename from 'sanitize-filename'; +import { DEFAULT_BREW } from './brewDefaults.js'; +import { splitTextStyleAndMetadata } from '../shared/helpers.js'; + +export default function pageRoutes({ + defaultMetaTags, + welcomeText, + welcomeTextLegacy, + migrateText, + changelogText, + faqText, + getBrew, + getUsersBrewThemes, + HomebrewModel, + GoogleActions, + sanitizeBrew, +}) { + const app = express.Router(); + + //Home page + app.get('/', (req, res, next)=>{ + req.brew = { + text : welcomeText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'Homepage', + description : 'Homepage' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Home page Legacy + app.get('/legacy', (req, res, next)=>{ + req.brew = { + text : welcomeTextLegacy, + renderer : 'legacy', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'Homepage (Legacy)', + description : 'Homepage' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Legacy/Other Document -> v3 Migration Guide + app.get('/migrate', (req, res, next)=>{ + req.brew = { + text : migrateText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'v3 Migration Guide', + description : 'A brief guide to converting Legacy documents to the v3 renderer.' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Changelog page + app.get('/changelog', async (req, res, next)=>{ + req.brew = { + title : 'Changelog', + text : changelogText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'Changelog', + description : 'Development changelog.' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //FAQ page + app.get('/faq', async (req, res, next)=>{ + req.brew = { + title : 'FAQ', + text : faqText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'FAQ', + description : 'Frequently Asked Questions' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Source page + app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{ + const { brew } = req; + + const replaceStrings = { '&': '&', '<': '<', '>': '>' }; + let text = brew.text; + for (const replaceStr in replaceStrings) { + text = text.replaceAll(replaceStr, replaceStrings[replaceStr]); + } + text = `
${text}
`; + res.status(200).send(text); + }); + + //Download brew source page + app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{ + const { brew } = req; + sanitizeBrew(brew, 'share'); + const prefix = 'HB - '; + + const encodeRFC3986ValueChars = (str)=>{ + return ( + encodeURIComponent(str) + .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;}) + ); + }; + + let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); + if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; + res.set({ + 'Cache-Control' : 'no-cache', + 'Content-Type' : 'text/plain', + 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt` + }); + res.status(200).send(brew.text); + }); + + //User Page + app.get('/user/:username', dbCheck, async (req, res, next)=>{ + const ownAccount = req.account && (req.account.username == req.params.username); + + req.ogMeta = { ...defaultMetaTags, + title : `${req.params.username}'s Collection`, + description : 'View my collection of homebrew on the Homebrewery.' + // type : could be 'profile'? + }; + + const fields = [ + 'googleId', + 'title', + 'pageCount', + 'description', + 'authors', + 'lang', + 'published', + 'views', + 'shareId', + 'editId', + 'createdAt', + 'updatedAt', + 'lastViewed', + 'thumbnail', + 'tags' + ]; + + let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields) + .catch((err)=>{ + console.log(err); + }); + + brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed" + + if(ownAccount && req?.account?.googleId){ + const auth = await GoogleActions.authCheck(req.account, res); + let googleBrews = await GoogleActions.listGoogleBrews(auth) + .catch((err)=>{ + console.error(err); + }); + + // If stub matches file from Google, use Google metadata over stub metadata + if(googleBrews && googleBrews.length > 0) { + for (const brew of brews.filter((brew)=>brew.googleId)) { + const match = googleBrews.findIndex((b)=>b.editId === brew.editId); + if(match !== -1) { + brew.googleId = googleBrews[match].googleId; + brew.pageCount = googleBrews[match].pageCount; + brew.renderer = googleBrews[match].renderer; + brew.version = googleBrews[match].version; + brew.webViewLink = googleBrews[match].webViewLink; + googleBrews.splice(match, 1); + } + } + + //Remaining unstubbed google brews display current user as author + googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] })); + brews = _.concat(brews, googleBrews); + } + } + + req.brews = _.map(brews, (brew)=>{ + // Clean up brew data + brew.title = brew.title?.trim(); + brew.description = brew.description?.trim(); + return sanitizeBrew(brew, ownAccount ? 'edit' : 'share'); + }); + + return next(); + }); + + //Edit Page + app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{ + req.brew = req.brew.toObject ? req.brew.toObject() : req.brew; + + req.userThemes = await(getUsersBrewThemes(req.account?.username)); + + req.ogMeta = { ...defaultMetaTags, + title : req.brew.title || 'Untitled Brew', + description : req.brew.description || 'No description.', + image : req.brew.thumbnail || defaultMetaTags.image, + locale : req.brew.lang, + type : 'article' + }; + + sanitizeBrew(req.brew, 'edit'); + splitTextStyleAndMetadata(req.brew); + res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save. + return next(); + })); + + //New Page from ID + app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{ + sanitizeBrew(req.brew, 'share'); + splitTextStyleAndMetadata(req.brew); + const brew = { + shareId : req.brew.shareId, + title : `CLONE - ${req.brew.title}`, + text : req.brew.text, + style : req.brew.style, + renderer : req.brew.renderer, + theme : req.brew.theme, + tags : req.brew.tags, + snippets : req.brew.snippets + }; + req.brew = _.defaults(brew, DEFAULT_BREW); + + req.userThemes = await(getUsersBrewThemes(req.account?.username)); + + req.ogMeta = { ...defaultMetaTags, + title : 'New', + description : 'Start crafting your homebrew on the Homebrewery!' + }; + + return next(); + })); + + //New Page + app.get('/new', asyncHandler(async(req, res, next)=>{ + req.userThemes = await(getUsersBrewThemes(req.account?.username)); + + req.ogMeta = { ...defaultMetaTags, + title : 'New', + description : 'Start crafting your homebrew on the Homebrewery!' + }; + + return next(); + })); + + //Share Page + app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ + const { brew } = req; + req.ogMeta = { ...defaultMetaTags, + title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, + description : req.brew.description || 'No description.', + image : req.brew.thumbnail || defaultMetaTags.image, + type : 'article' + }; + + // increase visitor view count, do not include visits by author(s) + if(!brew.authors.includes(req.account?.username)){ + if(req.params.id.length > 12 && !brew._id) { + const googleId = brew.googleId; + const shareId = brew.shareId; + await GoogleActions.increaseView(googleId, shareId, 'share', brew) + .catch((err)=>{next(err);}); + } else { + await HomebrewModel.increaseView({ shareId: brew.shareId }); + } + }; + + brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share'); + splitTextStyleAndMetadata(req.brew); + return next(); + })); + + //Account Page + app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ + const data = {}; + data.title = 'Account Information Page'; + + if(!req.account) { + res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"'); + const error = new Error('No valid account'); + error.status = 401; + error.HBErrorCode = '50'; + error.page = data.title; + return next(error); + }; + + let auth; + let googleCount = []; + if(req.account) { + if(req.account.googleId) { + auth = await GoogleActions.authCheck(req.account, res, false); + + googleCount = await GoogleActions.listGoogleBrews(auth) + .catch((err)=>{ + console.error(err); + }); + } + + const query = { authors: req.account.username, googleId: { $exists: false } }; + const mongoCount = await HomebrewModel.countDocuments(query) + .catch((err)=>{ + console.log(err); + return 0; + }); + + data.accountDetails = { + username : req.account.username, + issued : req.account.issued, + googleId : Boolean(req.account.googleId), + authCheck : Boolean(req.account.googleId && auth?.credentials.access_token), + mongoCount : mongoCount, + googleCount : googleCount?.length + }; + } + + req.brew = data; + + req.ogMeta = { ...defaultMetaTags, + title : `Account Page`, + description : null + }; + + return next(); + })); + + //Vault Page + app.get('/vault', asyncHandler(async(req, res, next)=>{ + req.ogMeta = { ...defaultMetaTags, + title : 'The Vault', + description : 'Search for Brews' + }; + return next(); + })); + + return app; +} \ No newline at end of file From 18c29daf92fbd436183ba894e2ffd057f63ef666 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Sep 2026 13:20:45 -0400 Subject: [PATCH 21/37] Move resetWarnUnsavedTimer and ToggleAutosave to common hook --- client/homebrew/pages/editPage/editPage.jsx | 32 ++++++------------- client/homebrew/pages/homePage/homePage.jsx | 10 +----- client/homebrew/pages/newPage/newPage.jsx | 11 +------ .../homebrew/utils/commonEditPageFunctions.js | 25 +++++++++++++-- 4 files changed, 34 insertions(+), 44 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index dea5606f2..b9dbe9dce 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -39,8 +39,6 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes -const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; @@ -48,7 +46,7 @@ const SNIPKEY = 'HB_newPage_snippets'; const METAKEY = 'HB_newPage_meta'; const useLocalStorage = false; -const sandbox = false; +const sandbox = false; const EditPage = (props)=>{ props = { @@ -74,10 +72,9 @@ const EditPage = (props)=>{ const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); - const editorRef = useRef(null); - const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const saveTimeout = useRef(null); - const warnUnsavedTimeout = useRef(null); + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); + const saveTimeout = useRef(null); const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ ...prevBrew, @@ -86,12 +83,6 @@ const EditPage = (props)=>{ snippets : newData.snippets })); - const resetWarnUnsavedTimer = ()=>{ - setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds - clearTimeout(warnUnsavedTimeout.current); - warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings - }; - const handleGoogleClick = ()=>{ if(currentBrew.authors.length > 0 && global.account?.username !== currentBrew.authors[0]) { setAlertOwnershipToTransfer(true); @@ -272,14 +263,6 @@ const EditPage = (props)=>{ return saved; }; - const toggleAutoSave = ()=>{ - clearTimeout(warnUnsavedTimeout.current); - clearTimeout(saveTimeout.current); - localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled)); - setAutoSaveEnabled(!autoSaveEnabled); - setWarnUnsavedChanges(autoSaveEnabled); - }; - const renderAutoSaveButton = ()=>( Autosave @@ -317,8 +300,10 @@ const EditPage = (props)=>{ }; const { + resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + toggleAutoSave } = useCommonEditPageFunctions({ saveGoogle, setError, @@ -341,7 +326,8 @@ const EditPage = (props)=>{ trySave, sandbox, lastSavedBrew, - editorRef + editorRef, + saveTimeout }); return ( diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 1e02d67b8..7685f7bc5 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -32,8 +32,6 @@ import Headtags from '@vitreum/headtags.js'; const Meta = Headtags.Meta; const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes -const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; @@ -64,7 +62,6 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const warnUnsavedTimeout = useRef(null); const save = ()=>{ request.post('/api') @@ -79,12 +76,6 @@ const HomePage =(props)=>{ }); }; - const resetWarnUnsavedTimer = ()=>{ - setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds - clearTimeout(warnUnsavedTimeout.current); - warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings - }; - const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING if(isSaving) @@ -142,6 +133,7 @@ const HomePage =(props)=>{ }; const { + resetWarnUnsavedTimer, handleSplitMove, handleBrewChange } = useCommonEditPageFunctions({ diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 8182ac20c..9db9f11f2 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -29,8 +29,6 @@ const { both: RecentNavItem } = RecentNavItems; // Page specific imports const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes -const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; @@ -64,8 +62,6 @@ const NewPage = (props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - // const saveTimeout = useRef(null); - const warnUnsavedTimeout = useRef(null); useEffect(()=>{ loadBrew(); @@ -100,12 +96,6 @@ const NewPage = (props)=>{ window.history.replaceState({}, window.location.title, '/new/'); }; - const resetWarnUnsavedTimer = ()=>{ - setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds - clearTimeout(warnUnsavedTimeout.current); - warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings - }; - const trySave = useEffectEvent(async ()=>{ setIsSaving(true); @@ -196,6 +186,7 @@ const NewPage = (props)=>{ ); const { + resetWarnUnsavedTimer, handleSplitMove, handleBrewChange } = useCommonEditPageFunctions({ diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index ff53981f8..f0c398ae4 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -4,6 +4,9 @@ import _ from 'lodash'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; +const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds + export default function useCommonEditPageFunctions(dependencies) { const { setError, @@ -27,10 +30,12 @@ export default function useCommonEditPageFunctions(dependencies) { unsavedChanges, setUnsavedChanges, lastSavedBrew, - editorRef + editorRef, + saveTimeout = undefined } = dependencies; const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges + const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time //==--------- Page setup ----------==// useEffect(()=>{ @@ -70,6 +75,12 @@ export default function useCommonEditPageFunctions(dependencies) { if(autoSaveEnabled) trySave(false, hasChange, saveGoogle); }, [currentBrew]); + const resetWarnUnsavedTimer = ()=>{ + setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds + clearTimeout(warnUnsavedTimeout.current); + warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings + }; + const handleSplitMove = ()=>{ editorRef.current.update(); }; @@ -97,8 +108,18 @@ export default function useCommonEditPageFunctions(dependencies) { } }; + const toggleAutoSave = ()=>{ + clearTimeout(warnUnsavedTimeout.current); + clearTimeout(saveTimeout.current); + localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled)); + setAutoSaveEnabled(!autoSaveEnabled); + setWarnUnsavedChanges(autoSaveEnabled); + }; + return { + resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + toggleAutoSave } } \ No newline at end of file From 3e2ac93424d248461cb82eef465c3db7815c6849 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Sep 2026 16:24:45 -0400 Subject: [PATCH 22/37] Make trySave common and move to useCommonEditorFunctions Also makes "save()" more common between the pages, but they still have some differences --- client/homebrew/pages/editPage/editPage.jsx | 32 ++++----------- client/homebrew/pages/homePage/homePage.jsx | 40 +++++++++++-------- client/homebrew/pages/newPage/newPage.jsx | 40 ++++++++++--------- .../homebrew/utils/commonEditPageFunctions.js | 35 +++++++++++++--- 4 files changed, 82 insertions(+), 65 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index b9dbe9dce..a0287236b 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -38,8 +38,6 @@ import LockNotification from './lockNotification/lockNotification.jsx'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; import googleDriveIcon from '../../googleDrive.svg'; -const SAVE_TIMEOUT = 10000; - const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -74,7 +72,6 @@ const EditPage = (props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const saveTimeout = useRef(null); const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ ...prevBrew, @@ -113,25 +110,6 @@ const EditPage = (props)=>{ trySave(true, true, newSaveGoogle); }; - const trySave = useEffectEvent((immediate = false, hasChanges = true, saveToGoogle = false)=>{ - clearTimeout(saveTimeout.current); - if(isSaving) return; - if(!hasChanges && !immediate) return; - const newTimeout = immediate ? 0 : SAVE_TIMEOUT; - - saveTimeout.current = setTimeout(async ()=>{ - setIsSaving(true); - setError(null); - await save(currentBrew, saveToGoogle) - .catch((err)=>{ - setError(err); - }); - setIsSaving(false); - setLastSavedTime(new Date()); - if(!autoSaveEnabled) resetWarnUnsavedTimer(); - }, newTimeout); - }); - const save = async (brew, saveToGoogle)=>{ setHTMLErrors(hbfm.validate(brew.text)); @@ -303,7 +281,8 @@ const EditPage = (props)=>{ resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, - toggleAutoSave + toggleAutoSave, + trySave } = useCommonEditPageFunctions({ saveGoogle, setError, @@ -323,11 +302,14 @@ const EditPage = (props)=>{ setWarnUnsavedChanges, unsavedChanges, setUnsavedChanges, - trySave, sandbox, lastSavedBrew, editorRef, - saveTimeout + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime }); return ( diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 7685f7bc5..a1a4a257e 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -31,8 +31,6 @@ const { both: RecentNavItem } = RecentNavItems; import Headtags from '@vitreum/headtags.js'; const Meta = Headtags.Meta; -const SAVE_TIMEOUT = 10000; - const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -48,6 +46,7 @@ const HomePage =(props)=>{ }; const [currentBrew, setCurrentBrew] = useState(props.brew); + const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false); const [error, setError] = useState(undefined); const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text)); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1); @@ -63,17 +62,19 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const save = ()=>{ - request.post('/api') - .send(currentBrew) - .end((err, res)=>{ - if(err) { - setError(err); - return; - } - const saved = res.body; - window.location = `/edit/${saved.editId}`; + const save = async (brew, saveToGoogle)=>{ + const res = await request + .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) + .send(brew) + .catch((err)=>{ + console.error('Error Updating Local Brew'); + setError(err); }); + if(!res) return; + + const saved = res.body; + window.onbeforeunload = null; + window.location = `/edit/${saved.editId}`; }; const renderSaveButton = ()=>{ @@ -97,7 +98,7 @@ const HomePage =(props)=>{ // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) - return save now; + return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED if(autoSaveEnabled) @@ -135,8 +136,10 @@ const HomePage =(props)=>{ const { resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + trySave } = useCommonEditPageFunctions({ + saveGoogle, setError, setThemeBundle, HTMLErrors, @@ -156,7 +159,12 @@ const HomePage =(props)=>{ setUnsavedChanges, sandbox, lastSavedBrew, - editorRef + editorRef, + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime }); return ( @@ -190,7 +198,7 @@ const HomePage =(props)=>{ />
-
+
trySave(true, true, saveGoogle)}> Save current
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 9db9f11f2..1ec1720f4 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -28,8 +28,6 @@ import RecentNavItems from '@navbar/recent.navitem.jsx'; const { both: RecentNavItem } = RecentNavItems; // Page specific imports -const SAVE_TIMEOUT = 10000; - const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -96,24 +94,22 @@ const NewPage = (props)=>{ window.history.replaceState({}, window.location.title, '/new/'); }; - const trySave = useEffectEvent(async ()=>{ - setIsSaving(true); - - const updatedBrew = { ...currentBrew }; - splitTextStyleAndMetadata(updatedBrew); - - const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm; - updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1; + const save = async (brew, saveToGoogle)=>{ + //Prepare content to send to server + const brewToSave = { + ...brew, + text : brew.text.normalize('NFC'), + pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1, + textBin : undefined + }; const res = await request .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) - .send(updatedBrew) + .send(brewToSave) .catch((err)=>{ - setIsSaving(false); + console.error('Error Updating Local Brew'); setError(err); }); - - setIsSaving(false); if(!res) return; const savedBrew = res.body; @@ -123,7 +119,7 @@ const NewPage = (props)=>{ localStorage.removeItem(METAKEY); window.onbeforeunload = null; window.location = `/edit/${savedBrew.editId}`; - }); + }; const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING @@ -146,7 +142,7 @@ const NewPage = (props)=>{ // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) - return save now; + return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED if(autoSaveEnabled) @@ -188,8 +184,10 @@ const NewPage = (props)=>{ const { resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + trySave } = useCommonEditPageFunctions({ + saveGoogle, setError, setThemeBundle, HTMLErrors, @@ -207,10 +205,14 @@ const NewPage = (props)=>{ setWarnUnsavedChanges, unsavedChanges, setUnsavedChanges, - trySave, sandbox, lastSavedBrew, - editorRef + editorRef, + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime }); return ( diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index f0c398ae4..e49e29136 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -1,14 +1,16 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useEffectEvent, useRef } from 'react'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import _ from 'lodash'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; +const SAVE_TIMEOUT = 10000; //Autosave 10 seconds after last change const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds export default function useCommonEditPageFunctions(dependencies) { const { + saveGoogle, setError, setThemeBundle, HTMLErrors, @@ -24,18 +26,21 @@ export default function useCommonEditPageFunctions(dependencies) { autoSaveEnabled, setAutoSaveEnabled, setWarnUnsavedChanges, - trySave = ()=>{}, sandbox, - saveGoogle = false, unsavedChanges, setUnsavedChanges, lastSavedBrew, editorRef, - saveTimeout = undefined + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime } = dependencies; const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time + const saveTimeout = useRef(null); //==--------- Page setup ----------==// useEffect(()=>{ @@ -116,10 +121,30 @@ export default function useCommonEditPageFunctions(dependencies) { setWarnUnsavedChanges(autoSaveEnabled); }; + const trySave = useEffectEvent((forceSave = false, hasChanges = true, saveToGoogle = false)=>{ + clearTimeout(saveTimeout.current); + if(isSaving) return; + if(!forceSave && !hasChanges) return; + const newTimeout = forceSave ? 0 : SAVE_TIMEOUT; + + saveTimeout.current = setTimeout(async ()=>{ + setIsSaving(true); + setError(null); + await save(currentBrew, saveToGoogle) + .catch((err)=>{ + setError(err); + }); + setIsSaving(false); + setLastSavedTime(new Date()); + if(!autoSaveEnabled) resetWarnUnsavedTimer(); + }, newTimeout); + }); + return { resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, - toggleAutoSave + toggleAutoSave, + trySave, } } \ No newline at end of file From 321b8cfc1b803b7c56669aacefc42a5359054a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 23:12:56 +0200 Subject: [PATCH 23/37] simplify imports as suggested --- server/app.js | 24 +++++------------------- server/page-routes.js | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/server/app.js b/server/app.js index 9e04c7b5b..eb078a877 100644 --- a/server/app.js +++ b/server/app.js @@ -16,11 +16,11 @@ import path from 'path'; import fs from 'fs-extra'; import api from './homebrew.api.js'; -import pageRoutes from './page-routes.js'; -const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api; +const { homebrewApi, getBrew, getCSS } = api; import adminApi from './admin.api.js'; import vaultApi from './vault.api.js'; -import GoogleActions from './googleActions.js'; +import pageRoutes from './page-routes.js'; + import serveCompressedStaticAssets from './static-assets.mv.js'; import asyncHandler from 'express-async-handler'; import { model as HomebrewModel } from './homebrew.model.js'; @@ -113,12 +113,6 @@ export default async function createApp(vite) { app.use(adminApi(vite)); app.use(vaultApi); - const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); - const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8'); - const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8'); - const changelogText = fs.readFileSync('changelog.md', 'utf8'); - const faqText = fs.readFileSync('faq.md', 'utf8'); - String.prototype.replaceAll = function(s, r){return this.split(s).join(r);}; const defaultMetaTags = { @@ -131,16 +125,8 @@ export default async function createApp(vite) { app.use(pageRoutes({ defaultMetaTags, - welcomeText, - welcomeTextLegacy, - migrateText, - changelogText, - faqText, - getBrew, - getUsersBrewThemes, - HomebrewModel, - GoogleActions, - sanitizeBrew, + HomebrewModel, + sanitizeBrew, })); //Robots.txt diff --git a/server/page-routes.js b/server/page-routes.js index df9fc2c44..c357db7dc 100644 --- a/server/page-routes.js +++ b/server/page-routes.js @@ -15,18 +15,20 @@ import dbCheck from './middleware/dbCheck.js'; import sanitizeFilename from 'sanitize-filename'; import { DEFAULT_BREW } from './brewDefaults.js'; import { splitTextStyleAndMetadata } from '../shared/helpers.js'; +import GoogleActions from './googleActions.js'; + +import api from './homebrew.api.js'; +const { getBrew, getUsersBrewThemes } = api; + +const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); +const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8'); +const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8'); +const changelogText = fs.readFileSync('changelog.md', 'utf8'); +const faqText = fs.readFileSync('faq.md', 'utf8'); export default function pageRoutes({ defaultMetaTags, - welcomeText, - welcomeTextLegacy, - migrateText, - changelogText, - faqText, - getBrew, - getUsersBrewThemes, HomebrewModel, - GoogleActions, sanitizeBrew, }) { const app = express.Router(); From 402dfebcf0ccf6533ae7e0b59445b65245b89a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 23:18:10 +0200 Subject: [PATCH 24/37] add fs --- server/page-routes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/page-routes.js b/server/page-routes.js index c357db7dc..e2e7be193 100644 --- a/server/page-routes.js +++ b/server/page-routes.js @@ -9,13 +9,14 @@ process.chdir(`${__dirname}/..`); import _ from 'lodash'; import express from 'express'; import asyncHandler from 'express-async-handler'; +import fs from 'fs'; //==== Middleware Imports ====// import dbCheck from './middleware/dbCheck.js'; import sanitizeFilename from 'sanitize-filename'; import { DEFAULT_BREW } from './brewDefaults.js'; import { splitTextStyleAndMetadata } from '../shared/helpers.js'; -import GoogleActions from './googleActions.js'; +import GoogleActions from './googleActions.js'; import api from './homebrew.api.js'; const { getBrew, getUsersBrewThemes } = api; From c14d1833b15a1ebd6764ac2450f1511018201158 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 14 Sep 2026 20:33:54 +1000 Subject: [PATCH 25/37] add named aliases for first-letter drop-cap style --- themes/Legacy/5ePHB/style.less | 15 ++++++++------- themes/V3/5eDMG/style.less | 1 + themes/V3/5ePHB/snippets.js | 1 + themes/V3/5ePHB/style.less | 35 +++++++++++++++++----------------- themes/V3/Journal/style.less | 27 +++++++++++++------------- 5 files changed, 42 insertions(+), 37 deletions(-) diff --git a/themes/Legacy/5ePHB/style.less b/themes/Legacy/5ePHB/style.less index 41524c74c..e09e53682 100644 --- a/themes/Legacy/5ePHB/style.less +++ b/themes/Legacy/5ePHB/style.less @@ -115,13 +115,14 @@ body { counter-reset : phb-page-numbers; } font-size : 0.987cm; -webkit-column-span : all; -moz-column-span : all; - & + p::first-letter { - float : left; - font-family : 'Solberry'; - font-size : 10em; - line-height : 0.795em; - color : #222222; - } + } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + font-family : 'Solberry'; + font-size : 10em; + line-height : 0.795em; + color : #222222; } h2 { font-size : 0.705cm; } h3 { diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less index d79533c2c..8aad90315 100644 --- a/themes/V3/5eDMG/style.less +++ b/themes/V3/5eDMG/style.less @@ -14,6 +14,7 @@ .note table tbody tr:nth-child(odd) { background : #FFFFFF; } /* DROP CAP */ + .first-letter, .drop-cap h1 + p::first-letter { color : black; background-image : unset; diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index fee12e1f6..73ac5cb43 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -30,6 +30,7 @@ export default [ name : 'Tweak Drop Cap', icon : 'fas fa-sliders-h', gen : dedent`/* Drop Cap settings */ + .page .first-letter, .page .drop-cap, .page h1 + p::first-letter { font-family: SolberaImitationRemake; font-size: 3.5cm; diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 12601bb5d..28bde8a2f 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -85,23 +85,24 @@ line-height : 1em; -webkit-column-span : all; -moz-column-span : all; - & + p::first-letter { - float : left; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'SolberaImitationRemake'; - font-size : 3.5cm; - line-height : 1em; - color : rgba(0, 0, 0, 0); - background-image : linear-gradient(-45deg, #322814, #998250, #322814); - -webkit-background-clip : text; - background-clip : text; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'SolberaImitationRemake'; + font-size : 3.5cm; + line-height : 1em; + color : rgba(0, 0, 0, 0); + background-image : linear-gradient(-45deg, #322814, #998250, #322814); + -webkit-background-clip : text; + background-clip : text; + } h2 { //margin-top : 0px; //Font is misaligned. Shift up slightly //margin-bottom : 0.05cm; @@ -196,7 +197,7 @@ } & + * { margin-top : 0.54cm; } - + } // ***************************** @@ -279,7 +280,7 @@ .watermark { color : black; } /* Watercolor */ - + .watercolor1 { --wc : @watercolor1; } .watercolor2 { --wc : @watercolor2; } .watercolor3 { --wc : @watercolor3; } diff --git a/themes/V3/Journal/style.less b/themes/V3/Journal/style.less index 74c976f47..9b9b7d024 100644 --- a/themes/V3/Journal/style.less +++ b/themes/V3/Journal/style.less @@ -80,21 +80,22 @@ font-size : 0.89cm; font-variant : small-caps; line-height : 1em; - & + p::first-letter { - float : left; - padding-top : 0.3em; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-right : 0.1em; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'FrederickaTheGreat'; - font-size : 1.9em; - line-height : 1em; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-top : 0.3em; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-right : 0.1em; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'FrederickaTheGreat'; + font-size : 1.9em; + line-height : 1em; + } h2 { font-size : 0.62cm; line-height : 0.988em; //Font is misaligned. Shift up slightly From a9be3d913dc6ac147ffb2b14b187cca845961ec9 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 14 Sep 2026 22:29:03 +1000 Subject: [PATCH 26/37] Revert "add named aliases for first-letter drop-cap style" This reverts commit c14d1833b15a1ebd6764ac2450f1511018201158. --- themes/V3/5eDMG/style.less | 1 - themes/V3/5ePHB/snippets.js | 1 - themes/V3/5ePHB/style.less | 31 +++++++++++++++---------------- themes/V3/Journal/style.less | 27 +++++++++++++-------------- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less index 8aad90315..d79533c2c 100644 --- a/themes/V3/5eDMG/style.less +++ b/themes/V3/5eDMG/style.less @@ -14,7 +14,6 @@ .note table tbody tr:nth-child(odd) { background : #FFFFFF; } /* DROP CAP */ - .first-letter, .drop-cap h1 + p::first-letter { color : black; background-image : unset; diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index 73ac5cb43..fee12e1f6 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -30,7 +30,6 @@ export default [ name : 'Tweak Drop Cap', icon : 'fas fa-sliders-h', gen : dedent`/* Drop Cap settings */ - .page .first-letter, .page .drop-cap, .page h1 + p::first-letter { font-family: SolberaImitationRemake; font-size: 3.5cm; diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 28bde8a2f..694c33d15 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -85,24 +85,23 @@ line-height : 1em; -webkit-column-span : all; -moz-column-span : all; + & + p::first-letter { + float : left; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'SolberaImitationRemake'; + font-size : 3.5cm; + line-height : 1em; + color : rgba(0, 0, 0, 0); + background-image : linear-gradient(-45deg, #322814, #998250, #322814); + -webkit-background-clip : text; + background-clip : text; + } & + p::first-line { font-variant : small-caps; } } - .first-letter, .drop-cap, - h1 + p::first-letter { - float : left; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'SolberaImitationRemake'; - font-size : 3.5cm; - line-height : 1em; - color : rgba(0, 0, 0, 0); - background-image : linear-gradient(-45deg, #322814, #998250, #322814); - -webkit-background-clip : text; - background-clip : text; - } h2 { //margin-top : 0px; //Font is misaligned. Shift up slightly //margin-bottom : 0.05cm; diff --git a/themes/V3/Journal/style.less b/themes/V3/Journal/style.less index 9b9b7d024..74c976f47 100644 --- a/themes/V3/Journal/style.less +++ b/themes/V3/Journal/style.less @@ -80,22 +80,21 @@ font-size : 0.89cm; font-variant : small-caps; line-height : 1em; + & + p::first-letter { + float : left; + padding-top : 0.3em; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-right : 0.1em; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'FrederickaTheGreat'; + font-size : 1.9em; + line-height : 1em; + } & + p::first-line { font-variant : small-caps; } } - .first-letter, .drop-cap, - h1 + p::first-letter { - float : left; - padding-top : 0.3em; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-right : 0.1em; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'FrederickaTheGreat'; - font-size : 1.9em; - line-height : 1em; - } h2 { font-size : 0.62cm; line-height : 0.988em; //Font is misaligned. Shift up slightly From 79df1dc9162314a57617f203239fe9913b9e35e4 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 15 Sep 2026 00:43:55 +1000 Subject: [PATCH 27/37] Reapply "add named aliases for first-letter drop-cap style" This reverts commit a9be3d913dc6ac147ffb2b14b187cca845961ec9. --- themes/V3/5eDMG/style.less | 1 + themes/V3/5ePHB/snippets.js | 1 + themes/V3/5ePHB/style.less | 31 ++++++++++++++++--------------- themes/V3/Journal/style.less | 27 ++++++++++++++------------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less index d79533c2c..8aad90315 100644 --- a/themes/V3/5eDMG/style.less +++ b/themes/V3/5eDMG/style.less @@ -14,6 +14,7 @@ .note table tbody tr:nth-child(odd) { background : #FFFFFF; } /* DROP CAP */ + .first-letter, .drop-cap h1 + p::first-letter { color : black; background-image : unset; diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index fee12e1f6..73ac5cb43 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -30,6 +30,7 @@ export default [ name : 'Tweak Drop Cap', icon : 'fas fa-sliders-h', gen : dedent`/* Drop Cap settings */ + .page .first-letter, .page .drop-cap, .page h1 + p::first-letter { font-family: SolberaImitationRemake; font-size: 3.5cm; diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 694c33d15..28bde8a2f 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -85,23 +85,24 @@ line-height : 1em; -webkit-column-span : all; -moz-column-span : all; - & + p::first-letter { - float : left; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'SolberaImitationRemake'; - font-size : 3.5cm; - line-height : 1em; - color : rgba(0, 0, 0, 0); - background-image : linear-gradient(-45deg, #322814, #998250, #322814); - -webkit-background-clip : text; - background-clip : text; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'SolberaImitationRemake'; + font-size : 3.5cm; + line-height : 1em; + color : rgba(0, 0, 0, 0); + background-image : linear-gradient(-45deg, #322814, #998250, #322814); + -webkit-background-clip : text; + background-clip : text; + } h2 { //margin-top : 0px; //Font is misaligned. Shift up slightly //margin-bottom : 0.05cm; diff --git a/themes/V3/Journal/style.less b/themes/V3/Journal/style.less index 74c976f47..9b9b7d024 100644 --- a/themes/V3/Journal/style.less +++ b/themes/V3/Journal/style.less @@ -80,21 +80,22 @@ font-size : 0.89cm; font-variant : small-caps; line-height : 1em; - & + p::first-letter { - float : left; - padding-top : 0.3em; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-right : 0.1em; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'FrederickaTheGreat'; - font-size : 1.9em; - line-height : 1em; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-top : 0.3em; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-right : 0.1em; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'FrederickaTheGreat'; + font-size : 1.9em; + line-height : 1em; + } h2 { font-size : 0.62cm; line-height : 0.988em; //Font is misaligned. Shift up slightly From e44706852cfedc3ac0684f7af600dafd3b361967 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 15 Sep 2026 00:55:14 +1000 Subject: [PATCH 28/37] revert legacy file changes --- themes/Legacy/5ePHB/style.less | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/themes/Legacy/5ePHB/style.less b/themes/Legacy/5ePHB/style.less index e09e53682..41524c74c 100644 --- a/themes/Legacy/5ePHB/style.less +++ b/themes/Legacy/5ePHB/style.less @@ -115,14 +115,13 @@ body { counter-reset : phb-page-numbers; } font-size : 0.987cm; -webkit-column-span : all; -moz-column-span : all; - } - .first-letter, .drop-cap, - h1 + p::first-letter { - float : left; - font-family : 'Solberry'; - font-size : 10em; - line-height : 0.795em; - color : #222222; + & + p::first-letter { + float : left; + font-family : 'Solberry'; + font-size : 10em; + line-height : 0.795em; + color : #222222; + } } h2 { font-size : 0.705cm; } h3 { From 13731f57710809ba097438a3c39efd4e9e9df272 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 14 Sep 2026 19:11:14 +0000 Subject: [PATCH 29/37] Move clearError to common hook --- client/homebrew/pages/editPage/editPage.jsx | 6 +----- client/homebrew/pages/homePage/homePage.jsx | 6 +----- client/homebrew/pages/newPage/newPage.jsx | 6 +----- client/homebrew/utils/commonEditPageFunctions.js | 6 ++++++ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index a0287236b..1c1cceb7a 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -247,11 +247,6 @@ const EditPage = (props)=>{ ); - const clearError = ()=>{ - setError(null); - setIsSaving(false); - }; - const renderNavbar = ()=>{ return @@ -282,6 +277,7 @@ const EditPage = (props)=>{ handleSplitMove, handleBrewChange, toggleAutoSave, + clearError, trySave } = useCommonEditPageFunctions({ saveGoogle, diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index a1a4a257e..b7e6bff82 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -112,11 +112,6 @@ const HomePage =(props)=>{ return saved; }; - const clearError = ()=>{ - setError(null); - setIsSaving(false); - }; - const renderNavbar = ()=>{ return @@ -137,6 +132,7 @@ const HomePage =(props)=>{ resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, + clearError, trySave } = useCommonEditPageFunctions({ saveGoogle, diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 1ec1720f4..2e756bc0d 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -156,11 +156,6 @@ const NewPage = (props)=>{ return saved; }; - const clearError = ()=>{ - setError(null); - setIsSaving(false); - }; - const renderNavbar = ()=>( @@ -185,6 +180,7 @@ const NewPage = (props)=>{ resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, + clearError, trySave } = useCommonEditPageFunctions({ saveGoogle, diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index e49e29136..7d23a1fb4 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -121,6 +121,11 @@ export default function useCommonEditPageFunctions(dependencies) { setWarnUnsavedChanges(autoSaveEnabled); }; + const clearError = ()=>{ + setError(null); + setIsSaving(false); + }; + const trySave = useEffectEvent((forceSave = false, hasChanges = true, saveToGoogle = false)=>{ clearTimeout(saveTimeout.current); if(isSaving) return; @@ -145,6 +150,7 @@ export default function useCommonEditPageFunctions(dependencies) { handleSplitMove, handleBrewChange, toggleAutoSave, + clearError, trySave, } } \ No newline at end of file From 7c2344460f5e71788084bd2b771dc22502c4e105 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 14 Sep 2026 21:08:04 +0000 Subject: [PATCH 30/37] Move RenderSaveButton over --- client/homebrew/pages/editPage/editPage.jsx | 52 +------------------ client/homebrew/pages/homePage/homePage.jsx | 52 +------------------ client/homebrew/pages/newPage/newPage.jsx | 52 +------------------ .../homebrew/utils/commonEditPageFunctions.js | 50 ++++++++++++++---- 4 files changed, 44 insertions(+), 162 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 1c1cceb7a..d8f550ff4 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -53,8 +53,6 @@ const EditPage = (props)=>{ }; const [currentBrew, setCurrentBrew] = useState(props.brew); - const [isSaving, setIsSaving] = useState(false); - const [lastSavedTime, setLastSavedTime] = useState(new Date()); const [saveGoogle, setSaveGoogle] = useState(!!props.brew.googleId); const [error, setError] = useState(null); const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text)); @@ -62,13 +60,10 @@ const EditPage = (props)=>{ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle, setThemeBundle] = useState({}); - const [unsavedChanges, setUnsavedChanges] = useState(false); const [alertTrashedGoogleBrew, setAlertTrashedGoogleBrew] = useState(props.brew.trashed); const [alertNoGoogleToTransfer, setAlertNoGoogleToTransfer] = useState(false); const [alertOwnershipToTransfer, setAlertOwnershipToTransfer] = useState(false); const [confirmGoogleTransfer, setConfirmGoogleTransfer] = useState(false); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); - const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -206,41 +201,6 @@ const EditPage = (props)=>{ ); - const renderSaveButton = ()=>{ - // #1 - Currently saving, show SAVING - if(isSaving) - return saving...; - - // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(unsavedChanges && warnUnsavedChanges) { - resetWarnUnsavedTimer(); - const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; - - return - Reminder... -
{text}
-
; - } - - // #3 - Unsaved changes exist, click to save, show SAVE NOW - if(unsavedChanges) - return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; - - // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(autoSaveEnabled) - return auto-saved; - - // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button - if(sandbox) - return save now; - - // DEFAULT - No unsaved changes, show SAVED - return saved; - }; - const renderAutoSaveButton = ()=>( Autosave @@ -273,11 +233,12 @@ const EditPage = (props)=>{ }; const { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, toggleAutoSave, clearError, + renderSaveButton, + autoSaveEnabled, trySave } = useCommonEditPageFunctions({ saveGoogle, @@ -293,19 +254,10 @@ const EditPage = (props)=>{ SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - unsavedChanges, - setUnsavedChanges, sandbox, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime }); return ( diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index b7e6bff82..d571c74ea 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -53,11 +53,6 @@ const HomePage =(props)=>{ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle, setThemeBundle] = useState({}); - const [unsavedChanges, setUnsavedChanges] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [lastSavedTime, setLastSavedTime] = useState(new Date()); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); - const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -77,41 +72,6 @@ const HomePage =(props)=>{ window.location = `/edit/${saved.editId}`; }; - const renderSaveButton = ()=>{ - // #1 - Currently saving, show SAVING - if(isSaving) - return saving...; - - // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(unsavedChanges && warnUnsavedChanges) { - resetWarnUnsavedTimer(); - const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; - - return - Reminder... -
{text}
-
; - } - - // #3 - Unsaved changes exist, click to save, show SAVE NOW - if(unsavedChanges) - return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; - - // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(autoSaveEnabled) - return auto-saved; - - // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button - if(sandbox) - return save now; - - // DEFAULT - No unsaved changes, show SAVED - return saved; - }; - const renderNavbar = ()=>{ return @@ -129,10 +89,11 @@ const HomePage =(props)=>{ }; const { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, clearError, + renderSaveButton, + unsavedChanges, trySave } = useCommonEditPageFunctions({ saveGoogle, @@ -148,19 +109,10 @@ const HomePage =(props)=>{ SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - unsavedChanges, - setUnsavedChanges, sandbox, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime }); return ( diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 2e756bc0d..bbeb1ed52 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -45,8 +45,6 @@ const NewPage = (props)=>{ }; const [currentBrew, setCurrentBrew] = useState(props.brew); - const [isSaving, setIsSaving] = useState(false); - const [lastSavedTime, setLastSavedTime] = useState(new Date()); const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false); const [error, setError] = useState(null); const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text)); @@ -54,9 +52,6 @@ const NewPage = (props)=>{ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle, setThemeBundle] = useState({}); - const [unsavedChanges, setUnsavedChanges] = useState(false); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); - const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -121,41 +116,6 @@ const NewPage = (props)=>{ window.location = `/edit/${savedBrew.editId}`; }; - const renderSaveButton = ()=>{ - // #1 - Currently saving, show SAVING - if(isSaving) - return saving...; - - // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(unsavedChanges && warnUnsavedChanges) { - resetWarnUnsavedTimer(); - const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; - - return - Reminder... -
{text}
-
; - } - - // #3 - Unsaved changes exist, click to save, show SAVE NOW - if(unsavedChanges) - return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; - - // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(autoSaveEnabled) - return auto-saved; - - // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button - if(sandbox) - return save now; - - // DEFAULT - No unsaved changes, show SAVED - return saved; - }; - const renderNavbar = ()=>( @@ -177,11 +137,10 @@ const NewPage = (props)=>{ ); const { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, clearError, - trySave + renderSaveButton } = useCommonEditPageFunctions({ saveGoogle, setError, @@ -196,19 +155,10 @@ const NewPage = (props)=>{ SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - unsavedChanges, - setUnsavedChanges, sandbox, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime }); return ( diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index 7d23a1fb4..89cd2e35e 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useEffectEvent, useRef } from 'react'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import _ from 'lodash'; +import Nav from '@navbar/nav.jsx'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; @@ -23,21 +24,18 @@ export default function useCommonEditPageFunctions(dependencies) { SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, sandbox, - unsavedChanges, - setUnsavedChanges, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime } = dependencies; + const [isSaving, setIsSaving] = useState(false); + const [lastSavedTime, setLastSavedTime] = useState(new Date()); + const [autoSaveEnabled, setAutoSaveEnabled] = useState(!sandbox); + const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); + const [unsavedChanges, setUnsavedChanges] = useState(false); + const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time const saveTimeout = useRef(null); @@ -59,7 +57,6 @@ export default function useCommonEditPageFunctions(dependencies) { e.preventDefault(); } }; - document.addEventListener('keydown', handleControlKeys); window.onbeforeunload = ()=>{ if(unsavedChangesRef.current) @@ -145,12 +142,43 @@ export default function useCommonEditPageFunctions(dependencies) { }, newTimeout); }); + const renderSaveButton = ()=>{ + if(isSaving) + return saving...; + + if(unsavedChanges && warnUnsavedChanges) { + resetWarnUnsavedTimer(); + const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; + + return + Reminder... +
{text}
+
; + } + + if(unsavedChanges) + return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; + + if(autoSaveEnabled) + return auto-saved; + + if(sandbox) + return save now; + + return saved; + }; + return { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, toggleAutoSave, clearError, trySave, + renderSaveButton, + autoSaveEnabled, + unsavedChanges, } } \ No newline at end of file From 2676abe4e7c142bc7ca5a788b902d7fd92870462 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 14 Sep 2026 20:51:09 -0400 Subject: [PATCH 31/37] Change hook from js to jsx --- client/homebrew/pages/editPage/editPage.jsx | 2 +- client/homebrew/pages/homePage/homePage.jsx | 2 +- client/homebrew/pages/newPage/newPage.jsx | 2 +- ...onEditPageFunctions.js => commonEditPageFunctions.jsx} | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) rename client/homebrew/utils/{commonEditPageFunctions.js => commonEditPageFunctions.jsx} (95%) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d8f550ff4..205cf3fcb 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -9,7 +9,7 @@ import _ from 'lodash'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; -import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx' import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index d571c74ea..0132a1147 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -9,7 +9,7 @@ import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; -import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx' import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index bbeb1ed52..48d589c1e 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -10,7 +10,7 @@ import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js'; -import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx' import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.jsx similarity index 95% rename from client/homebrew/utils/commonEditPageFunctions.js rename to client/homebrew/utils/commonEditPageFunctions.jsx index 89cd2e35e..c8ecbaa0c 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.jsx @@ -30,11 +30,11 @@ export default function useCommonEditPageFunctions(dependencies) { save, } = dependencies; - const [isSaving, setIsSaving] = useState(false); - const [lastSavedTime, setLastSavedTime] = useState(new Date()); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(!sandbox); + const [isSaving , setIsSaving] = useState(false); + const [lastSavedTime , setLastSavedTime] = useState(new Date()); + const [autoSaveEnabled , setAutoSaveEnabled] = useState(!sandbox); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); - const [unsavedChanges, setUnsavedChanges] = useState(false); + const [unsavedChanges , setUnsavedChanges] = useState(false); const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time From 3e068129bbcd5bd0f9684f1e760e92b6ba6eecb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 16 Sep 2026 12:26:39 +0200 Subject: [PATCH 32/37] sum of all changes --- client/homebrew/navbar/navbar.jsx | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/client/homebrew/navbar/navbar.jsx b/client/homebrew/navbar/navbar.jsx index 40e5aef71..c55672d15 100644 --- a/client/homebrew/navbar/navbar.jsx +++ b/client/homebrew/navbar/navbar.jsx @@ -1,34 +1,28 @@ import './navbar.less'; import React from 'react'; -import createReactClass from 'create-react-class'; import Nav from './nav.jsx'; import PatreonNavItem from './patreon.navitem.jsx'; -const Navbar = createReactClass({ - displayName : 'Navbar', - getInitialState : function() { - return { - ver : global.version || '0.0.0' - }; - }, +const Navbar = ({ children })=>{ + const version = global.version || '0.0.0'; - render : function(){ - return + return ( +
The Homebrewery
- {`v${this.state.ver}`} + {`v${version}`} - {/*this.renderChromeWarning()*/} + {/* this.renderChromeWarning() */}
- {this.props.children} -
; - } -}); + {children} +
+ ); +}; -export default Navbar; +export default Navbar; \ No newline at end of file From 7a9e561742754e3325b579737404a8377e2013a8 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 18 Sep 2026 10:06:48 -0400 Subject: [PATCH 33/37] Change visiblePages and centerPage to separate states Allows small logic cleanup in handlePageVisibilityChange --- client/homebrew/brewRenderer/brewRenderer.jsx | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 65de45177..577d683f8 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -111,11 +111,12 @@ const BrewRenderer = (props)=>{ ...props }; + const [visiblePages, setVisiblePages] = useState([]); + const [centerPage , setCenterPage ] = useState(1); + const [state, setState] = useState({ isMounted : false, - visibility : 'hidden', - visiblePages : [], - centerPage : 1 + visibility : 'hidden' }); const [displayOptions, setDisplayOptions] = useState({ @@ -145,21 +146,17 @@ const BrewRenderer = (props)=>{ rawPages = props.text.split(PAGEBREAK_REGEX_V3); } - const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{ - setState((prevState)=>{ - const updatedVisiblePages = new Set(prevState.visiblePages); - if(!isCenter) - isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); - - return { - ...prevState, - visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b), - centerPage : isCenter ? pageNum : prevState.centerPage - }; + const handlePageVisibilityChange = (pageNum, isVisible, isCenter) => { + setVisiblePages(prev => { + const updatedVisiblePages = new Set(prev); + isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); + return [...updatedVisiblePages].sort((a, b) => a - b); }); - if(isCenter) + if (isCenter) { + setCenterPage(pageNum); props.onPageChange(pageNum); + } }; const isInView = (index)=>{ @@ -346,7 +343,7 @@ const BrewRenderer = (props)=>{
- 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> + 0 ? visiblePages : [centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> {/*render in iFrame so broken code doesn't crash the site.*/} Date: Fri, 18 Sep 2026 10:26:24 -0400 Subject: [PATCH 34/37] Remove unused brewRenderer props --- client/homebrew/brewRenderer/brewRenderer.jsx | 4 ---- client/homebrew/pages/editPage/editPage.jsx | 2 -- client/homebrew/pages/homePage/homePage.jsx | 3 +-- client/homebrew/pages/newPage/newPage.jsx | 2 -- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 577d683f8..0f02a50b4 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -23,7 +23,6 @@ import safeHTML from './safeHTML.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m; const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m; -const PAGE_HEIGHT = 1056; const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState'; @@ -100,11 +99,9 @@ const BrewRenderer = (props)=>{ text : '', style : '', renderer : 'legacy', - theme : '5ePHB', lang : '', errors : [], currentEditorCursorPageNum : 1, - currentEditorViewPageNum : 1, currentBrewRendererPageNum : 1, themeBundle : {}, onPageChange : ()=>{}, @@ -138,7 +135,6 @@ const BrewRenderer = (props)=>{ const mainRef = useRef(null); const pagesRef = useRef(null); - const urlRef = useRef(''); if(props.renderer == 'legacy') { rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY); diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 205cf3fcb..57667ca6e 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -289,12 +289,10 @@ const EditPage = (props)=>{ text={currentBrew.text} style={currentBrew.style} renderer={currentBrew.renderer} - theme={currentBrew.theme} themeBundle={themeBundle} errors={HTMLErrors} lang={currentBrew.lang} onPageChange={setCurrentBrewRendererPageNum} - currentEditorViewPageNum={currentEditorViewPageNum} currentEditorCursorPageNum={currentEditorCursorPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum} allowPrint={true} diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 0132a1147..0ac78f0e1 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -138,11 +138,10 @@ const HomePage =(props)=>{ text={currentBrew.text} style={currentBrew.style} renderer={currentBrew.renderer} + themeBundle={themeBundle} onPageChange={setCurrentBrewRendererPageNum} - currentEditorViewPageNum={currentEditorViewPageNum} currentEditorCursorPageNum={currentEditorCursorPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum} - themeBundle={themeBundle} />
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 48d589c1e..c7b6c7160 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -183,12 +183,10 @@ const NewPage = (props)=>{ text={currentBrew.text} style={currentBrew.style} renderer={currentBrew.renderer} - theme={currentBrew.theme} themeBundle={themeBundle} errors={HTMLErrors} lang={currentBrew.lang} onPageChange={setCurrentBrewRendererPageNum} - currentEditorViewPageNum={currentEditorViewPageNum} currentEditorCursorPageNum={currentEditorCursorPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum} allowPrint={true} From 0b6702c1988c2d383cad8fc50c5437829603f649 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 18 Sep 2026 10:35:21 -0400 Subject: [PATCH 35/37] [brewRenderer] Remove mainRef; it does nothing --- client/homebrew/brewRenderer/brewRenderer.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 0f02a50b4..6776389c1 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -133,7 +133,6 @@ const BrewRenderer = (props)=>{ const [headerState, setHeaderState] = useState(false); - const mainRef = useRef(null); const pagesRef = useRef(null); if(props.renderer == 'legacy') { @@ -334,7 +333,7 @@ const BrewRenderer = (props)=>{ : null} -
+
From 90c42dc51d3a445fd074779fc96d49615eaca3ac Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 18 Sep 2026 11:17:36 -0400 Subject: [PATCH 36/37] Linting --- client/homebrew/brewRenderer/brewRenderer.jsx | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 6776389c1..b198fa615 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -46,31 +46,25 @@ const BrewPage = (props)=>{ }; const pageRef = useRef(null); const cleanText = safeHTML(props.contents); + const pageNum = props.index + 1; useEffect(()=>{ if(!pageRef.current) return; - // Observer for tracking pages within the `.pages` div + // Observer for tracking which pages are at least 30% visible in the iframe const visibleObserver = new IntersectionObserver( - (entries)=>{ - entries.forEach((entry)=>{ - if(entry.isIntersecting) - props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages. - else - props.onVisibilityChange(props.index + 1, false, false); - }); - }, + (entries)=>entries.forEach((entry)=>{ + props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add page to array of visible pages. + }), { threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds. ); // Observer for tracking the page at the center of the iframe. const centerObserver = new IntersectionObserver( - (entries)=>{ - entries.forEach((entry)=>{ - if(entry.isIntersecting) - props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page - }); - }, + (entries)=>entries.forEach((entry)=>{ + if(entry.isIntersecting) + props.onVisibilityChange(pageNum, true, true); // Set this page as the center page + }), { threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center ); @@ -91,7 +85,7 @@ const BrewPage = (props)=>{ //v=====--------------------< Brew Renderer Component >-------------------=====v// let renderedPages = []; -let pageTemplates = []; +const pageTemplates = []; let rawPages = []; const BrewRenderer = (props)=>{ @@ -109,11 +103,11 @@ const BrewRenderer = (props)=>{ }; const [visiblePages, setVisiblePages] = useState([]); - const [centerPage , setCenterPage ] = useState(1); + const [centerPage, setCenterPage] = useState(1); const [state, setState] = useState({ - isMounted : false, - visibility : 'hidden' + isMounted : false, + visibility : 'hidden' }); const [displayOptions, setDisplayOptions] = useState({ @@ -141,14 +135,14 @@ const BrewRenderer = (props)=>{ rawPages = props.text.split(PAGEBREAK_REGEX_V3); } - const handlePageVisibilityChange = (pageNum, isVisible, isCenter) => { - setVisiblePages(prev => { + const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{ + setVisiblePages((prev)=>{ const updatedVisiblePages = new Set(prev); isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); - return [...updatedVisiblePages].sort((a, b) => a - b); + return [...updatedVisiblePages].sort((a, b)=>a - b); }); - if (isCenter) { + if(isCenter) { setCenterPage(pageNum); props.onPageChange(pageNum); } @@ -341,11 +335,11 @@ const BrewRenderer = (props)=>{ 0 ? visiblePages : [centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> {/*render in iFrame so broken code doesn't crash the site.*/} - {emitClick();}} - sandbox="allow-same-origin allow-modals allow-top-navigation" + sandbox='allow-same-origin allow-modals allow-top-navigation' >
Date: Fri, 18 Sep 2026 11:24:39 -0400 Subject: [PATCH 37/37] Update brewRenderer.jsx --- client/homebrew/brewRenderer/brewRenderer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index b198fa615..75a07c7d1 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -103,7 +103,7 @@ const BrewRenderer = (props)=>{ }; const [visiblePages, setVisiblePages] = useState([]); - const [centerPage, setCenterPage] = useState(1); + const [centerPage , setCenterPage ] = useState(1); const [state, setState] = useState({ isMounted : false,