From d94afa9c508db14b2028428e676ee2fac00a1a19 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 19:33:02 -0400 Subject: [PATCH 01/12] convert functions and states --- client/homebrew/pages/editPage/editPage.jsx | 618 +++++++++----------- 1 file changed, 272 insertions(+), 346 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 51196a444..00b88346f 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,449 +1,375 @@ /* eslint-disable max-lines */ -require('./editPage.less'); -const React = require('react'); -const _ = require('lodash'); -const createClass = require('create-react-class'); -import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; +import './editPage.less'; + +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import request from '../../utils/request-middleware.js'; +import Markdown from 'naturalcrit/markdown.js'; + +import _ from 'lodash';; +import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; -import request from '../../utils/request-middleware.js'; const { Meta } = require('vitreum/headtags'); -const Nav = require('naturalcrit/nav/nav.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); +import Nav from 'naturalcrit/nav/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 { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; -const NewBrew = require('../../navbar/newbrew.navitem.jsx'); -const HelpNavItem = require('../../navbar/help.navitem.jsx'); -const PrintNavItem = require('../../navbar/print.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const Account = require('../../navbar/account.navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const VaultNavItem = require('../../navbar/vault.navitem.jsx'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -const SplitPane = require('client/components/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import LockNotification from './lockNotification/lockNotification.jsx'; -const LockNotification = require('./lockNotification/lockNotification.jsx'); - -import Markdown from 'naturalcrit/markdown.js'; - -const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); -const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); +import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; -const googleDriveIcon = require('../../googleDrive.svg'); +import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const EditPage = createClass({ - displayName : 'EditPage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW_LOAD - }; - }, +const EditPage = (props) => { + props = { + brew: DEFAULT_BREW_LOAD, + ...props + }; + const editor = useRef(null); + const savedBrew = useRef(_.cloneDeep(props.brew)); + const warningTimer = useRef(null); - getInitialState : function() { - return { - brew : this.props.brew, - isSaving : false, - unsavedChanges : false, - alertTrashedGoogleBrew : this.props.brew.trashed, - alertLoginToTransfer : false, - saveGoogle : this.props.brew.googleId ? true : false, - confirmGoogleTransfer : false, - error : null, - htmlErrors : Markdown.validate(this.props.brew.text), - url : '', - autoSave : true, - autoSaveWarning : false, - unsavedTime : new Date(), - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - displayLockMessage : this.props.brew.lock || false, - themeBundle : {} - }; - }, + const [currentBrew , setCurrentBrew ] = useState(props.brew); + const [isSaving , setIsSaving ] = useState(false); + const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); + const [error , setError ] = useState(null); + const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); + 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 [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); + const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); + const [url , setUrl ] = useState(''); + const [autoSave , setAutoSave ] = useState(true); + const [autoSaveWarning , setAutoSaveWarning ] = useState(false); + const [unsavedTime , setUnsavedTime ] = useState(new Date()); + const [displayLockMessage , setDisplayLockMessage ] = useState(props.brew.lock || false); - editor : React.createRef(null), - savedBrew : null, + const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); - componentDidMount : function(){ - this.setState({ - url : window.location.href - }); + useEffect(() => { + setUrl(window.location.href); - this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy + const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); + setAutoSave(autoSavePref); + setAutoSaveWarning(!autoSavePref) + setHTMLErrors(Markdown.validate(currentBrew.text)); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{ - if(this.state.autoSave){ - this.trySave(); - } else { - this.setState({ autoSaveWarning: true }); - } - }); - - window.onbeforeunload = ()=>{ - if(this.state.isSaving || this.state.unsavedChanges){ + document.addEventListener('keydown', handleControlKeys); + window.onbeforeunload = () => { + if (isSaving || unsavedChanges) { return 'You have unsaved changes!'; } }; - this.setState((prevState)=>({ - htmlErrors : Markdown.validate(prevState.brew.text) - })); + return () => { + document.removeEventListener('keydown', handleControlKeys); + window.onbeforeunload = null; + }; + }, []); - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); + useEffect(() => { + const hasChange = !_.isEqual(currentBrew, savedBrew.current); + setUnsavedChanges(hasChange); + }, [currentBrew]); - document.addEventListener('keydown', this.handleControlKeys); - }, - componentWillUnmount : function() { - window.onbeforeunload = function(){}; - document.removeEventListener('keydown', this.handleControlKeys); - }, - componentDidUpdate : function(){ - const hasChange = this.hasChanges(); - if(this.state.unsavedChanges != hasChange){ - this.setState({ - unsavedChanges : hasChange - }); - } - }, - - handleControlKeys : function(e){ - if(!(e.ctrlKey || e.metaKey)) return; + const handleControlKeys = (e) => { + if (!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; - if(e.keyCode == S_KEY) this.trySave(true); - if(e.keyCode == P_KEY) printCurrentBrew(); - if(e.keyCode == P_KEY || e.keyCode == S_KEY){ + if (e.keyCode === S_KEY) trySave(true); + if (e.keyCode === P_KEY) printCurrentBrew(); + if (e.keyCode === S_KEY || e.keyCode === P_KEY) { e.stopPropagation(); e.preventDefault(); } - }, + }; - handleSplitMove : function(){ - this.editor.current.update(); - }, + const handleSplitMove = () => { + editor.current?.update(); + }; - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, + const handleEditorViewPageChange = (pageNumber) => { + setCurrentEditorViewPageNum(pageNumber); + }; - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + const handleEditorCursorPageChange = (pageNumber) => { + setCurrentEditorCursorPageNum(pageNumber); + }; - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: pageNumber }); - }, + const handleBrewRendererPageChange = (pageNumber) => { + setCurrentBrewRendererPageNum(pageNumber); + }; - handleTextChange : function(text){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(text); + const handleTextChange = (text) => { + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(text); - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); + if (autoSave) debounceSave(); + }; - handleSnipChange : function(snippet){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + const handleStyleChange = (style) => { + setCurrentBrew(prevBrew => ({ ...prevBrew, style })); + if (autoSave) debounceSave(); + }; - this.setState((prevState)=>({ - brew : { ...prevState.brew, snippets: snippet }, - unsavedChanges : true, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + const handleSnipChange = (snippet)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(snippet); - handleStyleChange : function(style){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); + if (autoSave) debounceSave(); + }; - handleMetaChange : function(metadata, field=undefined){ - if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme); + const handleMetaChange = (metadata, field = undefined) => { + if (field === 'theme' || field === 'renderer') + fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - ...metadata - } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setCurrentBrew(prev => ({ ...prev, ...metadata })); + if (autoSave) debounceSave(); + }; - hasChanges : function(){ - return !_.isEqual(this.state.brew, this.savedBrew); - }, - - updateBrew : function(newData){ - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - style : newData.style, - text : newData.text, - snippets : newData.snippets - } + const updateBrew = (newData) => + setCurrentBrew((prevBrew) => ({ + ...prevBrew, + style : newData.style, + text : newData.text, + snippets : newData.snippets })); - }, - - trySave : function(immediate=false){ - if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); - if(this.state.isSaving) - return; - - if(immediate) { - this.debounceSave(); - this.debounceSave.flush(); - return; - } - if(this.hasChanges()) - this.debounceSave(); - else - this.debounceSave.cancel(); - }, + const trySave = (immediate = false) => { + if (!debounceSave.current) return; + if (isSaving) return; - handleGoogleClick : function(){ - if(!global.account?.googleId) { - this.setState({ - alertLoginToTransfer : true - }); + const hasChange = !_.isEqual(currentBrew, savedBrew.current); + + if (immediate) { + debounceSave.current(); + debounceSave.current.flush?.(); return; } - this.setState((prevState)=>({ - confirmGoogleTransfer : !prevState.confirmGoogleTransfer - })); - this.setState({ - error : null - }); - }, - closeAlerts : function(event){ - event.stopPropagation(); //Only handle click once so alert doesn't reopen - this.setState({ - alertTrashedGoogleBrew : false, - alertLoginToTransfer : false, - confirmGoogleTransfer : false - }); - }, + if (hasChange) { + debounceSave.current(); + } else { + debounceSave.current.cancel?.(); + } + }; - toggleGoogleStorage : function(){ - this.setState((prevState)=>({ - saveGoogle : !prevState.saveGoogle, - error : null - }), ()=>this.trySave(true)); - }, + const handleGoogleClick = () => { + if (!global.account?.googleId) { + setAlertLoginToTransfer(true); + return; + } - save : async function(){ - if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); + setConfirmGoogleTransfer((prev) => !prev); + setError(null); + }; - const brewState = this.state.brew; // freeze the current state - const preSaveSnapshot = { ...brewState }; + const closeAlerts = (e) => { + e.stopPropagation(); //Only handle click once so alert doesn't reopen + setAlertTrashedGoogleBrew(false); + setAlertLoginToTransfer(false); + setConfirmGoogleTransfer(false); + }; - this.setState((prevState)=>({ - isSaving : true, - error : null, - htmlErrors : Markdown.validate(prevState.brew.text) - })); + const toggleGoogleStorage = () => { + setSaveGoogle((prev) => !prev); + setError(null); + trySave(true); + }; - await updateHistory(this.state.brew).catch(console.error); + const save = async () => { + debounceSave.current?.cancel?.(); + + setIsSaving(true); + setError(null); + setHTMLErrors(Markdown.validate(currentBrew.text)); + + await updateHistory(currentBrew).catch(console.error); await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server - const brew = { ...brewState }; - brew.text = brew.text.normalize('NFC'); - this.savedBrew.text = this.savedBrew.text.normalize('NFC'); - brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; - brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text))); - brew.hash = await md5(this.savedBrew.text); - //brew.text = undefined; - Temporary parallel path - brew.textBin = undefined; + const brewToSave = { + ...currentBrew, + text : currentBrew.text.normalize('NFC'), + pageCount: ((currentBrew.renderer === 'legacy' ? currentBrew.text.match(/\\page/g) : currentBrew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(currentBrew.text.normalize('NFC')))), + hash : await md5(savedBrew.current.text), + textBin : undefined + }; - const compressedBrew = gzipSync(strToU8(JSON.stringify(brew))); + const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); + const transfer = saveGoogle === _.isNil(currentBrew.googleId); + const params = transfer ? `?${saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; - const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); - const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; const res = await request - .put(`/api/update/${brew.editId}${params}`) + .put(`/api/update/${brewToSave.editId}${params}`) .set('Content-Encoding', 'gzip') .set('Content-Type', 'application/json') .send(compressedBrew) .catch((err)=>{ - console.log('Error Updating Local Brew'); - this.setState({ error: err }); + console.error('Error Updating Local Brew'); + setError(err); }); if(!res) return; - this.savedBrew = { - ...preSaveSnapshot, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version + const { googleId, editId, shareId, version } = res.body; + + savedBrew.current = { + ...currentBrew, + googleId: googleId ?? null, + editId, + shareId, + version }; - this.setState((prevState) => ({ - brew: { - ...prevState.brew, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version - }, - isSaving : false, - unsavedTime : new Date() - }), ()=>{ - this.setState({ unsavedChanges : this.hasChanges() }); - }); + setCurrentBrew(prev => ({ + ...prev, + googleId: googleId ?? null, + editId, + shareId, + version + })); - history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); - }, + setIsSaving(false); + setUnsavedTime(new Date()); + setUnsavedChanges(!_.isEqual(currentBrew, savedBrew.current)); - renderGoogleDriveIcon : function(){ - return - Google Drive icon + history.replaceState(null, null, `/edit/${editId}`); + }; - {this.state.confirmGoogleTransfer && -
- { this.state.saveGoogle - ? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?` - : `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?` - } + const renderGoogleDriveIcon = () => ( + + Google Drive icon + + {confirmGoogleTransfer && ( +
+ {saveGoogle + ? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?' + : 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
-
- Yes -
-
- No -
+
Yes
+
No
- } + )} - {this.state.alertLoginToTransfer && -
- You must be signed in to a Google account to transfer - between the homebrewery and Google Drive! - -
- Sign In -
+ {alertLoginToTransfer && ( +
- } + )} - {this.state.alertTrashedGoogleBrew && -
- This brew is currently in your Trash folder on Google Drive!
If you want to keep it, make sure to move it before it is deleted permanently!
-
- OK -
+ {alertTrashedGoogleBrew && ( +
+ This brew is currently in your Trash folder on Google Drive!
+ If you want to keep it, make sure to move it before it is deleted permanently!
+
OK
- } - ; - }, - - renderSaveButton : function(){ + )} + + ); + const renderSaveButton = () => { // #1 - Currently saving, show SAVING - if(this.state.isSaving){ + if (isSaving) return saving...; - } // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(this.state.unsavedChanges && this.state.autoSaveWarning){ - this.setAutosaveWarning(); - const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60); - const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + if (unsavedChanges && autoSaveWarning) { + setAutosaveWarning(); + const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? 'Autosave is OFF.' + : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; return - Reminder... -
- {text} -
-
; + Reminder... +
{text}
+ } // #3 - Unsaved changes exist, click to save, show SAVE NOW - // Use trySave(true) instead of save() to use debounced save function - if(this.state.unsavedChanges){ - return this.trySave(true)} color='blue' icon='fas fa-save'>Save Now; - } + if (unsavedChanges) + return trySave(true)} color='blue' icon='fas fa-save'>Save Now + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(this.state.autoSave){ + if (autoSave) return auto-saved.; - } + // DEFAULT - No unsaved changes, show SAVED return saved.; - }, + }; - handleAutoSave : function(){ - if(this.warningTimer) clearTimeout(this.warningTimer); - this.setState((prevState)=>({ - autoSave : !prevState.autoSave, - autoSaveWarning : prevState.autoSave - }), ()=>{ - localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave)); - }); - }, + const handleAutoSave = () => { + if (warningTimer.current) clearTimeout(warningTimer.current); + localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); + setAutoSave(!autoSave); + setAutoSaveWarning(false); + }; - setAutosaveWarning : function(){ - setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display - this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings - this.warningTimer; - }, + const resetAutosaveWarning = () => { + setTimeout(setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds + warningTimer.current = setTimeout(setAutoSaveWarning(true), 900000); // 15 minutes between unsaved changes warnings + }; - errorReported : function(error) { - this.setState({ - error - }); - }, + const renderAutoSaveButton = () => ( + + Autosave + + ); - renderAutoSaveButton : function(){ - return - Autosave - ; - }, + const processShareId = () => ( + currentBrew.googleId && !currentBrew.stubbed + ? currentBrew.googleId + currentBrew.shareId + : currentBrew.shareId + ); - processShareId : function() { - return this.state.brew.googleId && !this.state.brew.stubbed ? - this.state.brew.googleId + this.state.brew.shareId : - this.state.brew.shareId; - }, - - getRedditLink : function(){ - - const shareLink = this.processShareId(); - const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : ''; - const title = `${this.props.brew.title} ${systems}`; + const getRedditLink = () => { + const shareLink = processShareId(); + const systems = currentBrew.systems.length > 0 ? ` [${currentBrew.systems.join(' - ')}]` : ''; + const title = `${currentBrew.title} ${systems}`; const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. -**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; + **[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; - }, + }; - clearError : function(){ - setState({ - error : null, - isSaving : false - }) - }, + const clearError = () => { + setError(null); + setIsSaving(false); + }; renderNavbar : function(){ const shareLink = this.processShareId(); @@ -502,7 +428,7 @@ const EditPage = createClass({ onStyleChange={this.handleStyleChange} onSnipChange={this.handleSnipChange} onMetaChange={this.handleMetaChange} - reportError={this.errorReported} + reportError={setError} renderer={this.state.brew.renderer} userThemes={this.props.userThemes} themeBundle={this.state.themeBundle} From 597ce7cb48c3470474ed2335c9f028f61c256713 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:05:47 -0400 Subject: [PATCH 02/12] Convert renderNavBar and render --- client/homebrew/pages/editPage/editPage.jsx | 106 ++++++++++---------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 00b88346f..20ef1f006 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -42,7 +42,7 @@ const EditPage = (props) => { brew: DEFAULT_BREW_LOAD, ...props }; - const editor = useRef(null); + const editorRef = useRef(null); const savedBrew = useRef(_.cloneDeep(props.brew)); const warningTimer = useRef(null); @@ -107,7 +107,7 @@ const EditPage = (props) => { }; const handleSplitMove = () => { - editor.current?.update(); + editorRef.current?.update(); }; const handleEditorViewPageChange = (pageNumber) => { @@ -371,24 +371,23 @@ const EditPage = (props) => { setIsSaving(false); }; - renderNavbar : function(){ - const shareLink = this.processShareId(); + const renderNavbar = ()=>{ + const shareLink = processShareId(); return - {this.state.brew.title} + {currentBrew.title} - {this.renderGoogleDriveIcon()} - {this.state.error ? - : - - {this.renderSaveButton()} - {this.renderAutoSaveButton()} - - } - + {renderGoogleDriveIcon()} + {error + ? + : + {renderSaveButton()} + {renderAutoSaveButton()} + } + @@ -400,63 +399,64 @@ const EditPage = (props) => { {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}> copy url - + post to reddit - - + + - ; - }, + }; - render : function(){ - return
+ return ( +
- {this.renderNavbar()} - {this.props.brew.lock && } + {renderNavbar()} + + {currentBrew.lock && } +
- +
-
; - } -}); +
+ ); +}; module.exports = EditPage; From a75364c7f6fa335ba903daa2b3f6edb5d1f3fc06 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:06:16 -0400 Subject: [PATCH 03/12] remove unused displayLockMessage state --- client/homebrew/pages/editPage/editPage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 20ef1f006..f34d6dd9b 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -63,7 +63,6 @@ const EditPage = (props) => { const [autoSave , setAutoSave ] = useState(true); const [autoSaveWarning , setAutoSaveWarning ] = useState(false); const [unsavedTime , setUnsavedTime ] = useState(new Date()); - const [displayLockMessage , setDisplayLockMessage ] = useState(props.brew.lock || false); const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); From 883f59ff0dc835516f018415cfd5041b5e557c1b Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:13:21 -0400 Subject: [PATCH 04/12] rename `autosave` state to `autoSaveEnabled` --- client/homebrew/pages/editPage/editPage.jsx | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index f34d6dd9b..002b9f468 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -60,7 +60,7 @@ const EditPage = (props) => { const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); const [url , setUrl ] = useState(''); - const [autoSave , setAutoSave ] = useState(true); + const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); const [autoSaveWarning , setAutoSaveWarning ] = useState(false); const [unsavedTime , setUnsavedTime ] = useState(new Date()); @@ -70,7 +70,7 @@ const EditPage = (props) => { setUrl(window.location.href); const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); - setAutoSave(autoSavePref); + setAutoSaveEnabled(autoSavePref); setAutoSaveWarning(!autoSavePref) setHTMLErrors(Markdown.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -91,6 +91,8 @@ const EditPage = (props) => { useEffect(() => { const hasChange = !_.isEqual(currentBrew, savedBrew.current); setUnsavedChanges(hasChange); + + if(autoSaveEnabled) save(); }, [currentBrew]); const handleControlKeys = (e) => { @@ -128,12 +130,12 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const handleStyleChange = (style) => { setCurrentBrew(prevBrew => ({ ...prevBrew, style })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const handleSnipChange = (snippet)=>{ @@ -143,7 +145,7 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const handleMetaChange = (metadata, field = undefined) => { @@ -151,7 +153,7 @@ const EditPage = (props) => { fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); setCurrentBrew(prev => ({ ...prev, ...metadata })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const updateBrew = (newData) => @@ -306,7 +308,7 @@ const EditPage = (props) => { // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING if (unsavedChanges && autoSaveWarning) { - setAutosaveWarning(); + resetAutoSaveWarning(); const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 ? 'Autosave is OFF.' @@ -323,7 +325,7 @@ const EditPage = (props) => { return trySave(true)} color='blue' icon='fas fa-save'>Save Now // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if (autoSave) + if (autoSaveEnabled) return auto-saved.; // DEFAULT - No unsaved changes, show SAVED @@ -333,18 +335,18 @@ const EditPage = (props) => { const handleAutoSave = () => { if (warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); - setAutoSave(!autoSave); + setAutoSaveEnabled(!autoSaveEnabled); setAutoSaveWarning(false); }; - const resetAutosaveWarning = () => { + const resetAutoSaveWarning = () => { setTimeout(setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds warningTimer.current = setTimeout(setAutoSaveWarning(true), 900000); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( - Autosave + Autosave ); From 90a81237ec8fd5c6ddddde0f56f0f56691fe7c41 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:18:25 -0400 Subject: [PATCH 05/12] rename handleAutoSave to toggleAutoSave --- 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 002b9f468..95d367522 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -332,7 +332,7 @@ const EditPage = (props) => { return saved.; }; - const handleAutoSave = () => { + const toggleAutoSave = () => { if (warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); setAutoSaveEnabled(!autoSaveEnabled); @@ -345,7 +345,7 @@ const EditPage = (props) => { }; const renderAutoSaveButton = () => ( - + Autosave ); From 90f6e7ec372bd3bbdaaacecc2586dd1b1cf0ae97 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 01:57:13 -0400 Subject: [PATCH 06/12] Make autosaving work debouncing does not play nice with functional component. Any debounced function gets locked in as the original state, meaning we keep saving the original document and overwriting the current document when a save fires. Must pass in the parameters instead of pulling directly from state to work properly. --- client/homebrew/pages/editPage/editPage.jsx | 64 +++++++++------------ 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 95d367522..bfb268496 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import './editPage.less'; -import React, { useState, useEffect, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; @@ -35,16 +35,13 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; -const SAVE_TIMEOUT = 10000; +const SAVE_TIMEOUT = 5000; const EditPage = (props) => { props = { brew: DEFAULT_BREW_LOAD, ...props }; - const editorRef = useRef(null); - const savedBrew = useRef(_.cloneDeep(props.brew)); - const warningTimer = useRef(null); const [currentBrew , setCurrentBrew ] = useState(props.brew); const [isSaving , setIsSaving ] = useState(false); @@ -64,7 +61,10 @@ const EditPage = (props) => { const [autoSaveWarning , setAutoSaveWarning ] = useState(false); const [unsavedTime , setUnsavedTime ] = useState(new Date()); - const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); + const editorRef = useRef(null); + const savedBrew = useRef(_.cloneDeep(props.brew)); + const warningTimer = useRef(null); + const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); useEffect(() => { setUrl(window.location.href); @@ -92,7 +92,7 @@ const EditPage = (props) => { const hasChange = !_.isEqual(currentBrew, savedBrew.current); setUnsavedChanges(hasChange); - if(autoSaveEnabled) save(); + if(hasChange && autoSaveEnabled) trySave(); }, [currentBrew]); const handleControlKeys = (e) => { @@ -130,12 +130,10 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); - if (autoSaveEnabled) debounceSave(); }; const handleStyleChange = (style) => { setCurrentBrew(prevBrew => ({ ...prevBrew, style })); - if (autoSaveEnabled) debounceSave(); }; const handleSnipChange = (snippet)=>{ @@ -145,7 +143,6 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); - if (autoSaveEnabled) debounceSave(); }; const handleMetaChange = (metadata, field = undefined) => { @@ -153,7 +150,6 @@ const EditPage = (props) => { fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); setCurrentBrew(prev => ({ ...prev, ...metadata })); - if (autoSaveEnabled) debounceSave(); }; const updateBrew = (newData) => @@ -165,22 +161,21 @@ const EditPage = (props) => { })); const trySave = (immediate = false) => { - if (!debounceSave.current) return; + //debounceSave = _.debounce(save, SAVE_TIMEOUT); if (isSaving) return; const hasChange = !_.isEqual(currentBrew, savedBrew.current); if (immediate) { - debounceSave.current(); - debounceSave.current.flush?.(); + debounceSave(currentBrew, saveGoogle); + debounceSave.flush?.(); return; } - if (hasChange) { - debounceSave.current(); - } else { - debounceSave.current.cancel?.(); - } + if (hasChange) + debounceSave(currentBrew, saveGoogle); + else + debounceSave.cancel?.(); }; const handleGoogleClick = () => { @@ -206,29 +201,29 @@ const EditPage = (props) => { trySave(true); }; - const save = async () => { - debounceSave.current?.cancel?.(); + const save = async (brew, saveToGoogle) => { + debounceSave?.cancel?.(); setIsSaving(true); setError(null); - setHTMLErrors(Markdown.validate(currentBrew.text)); + setHTMLErrors(Markdown.validate(brew.text)); - await updateHistory(currentBrew).catch(console.error); + await updateHistory(brew).catch(console.error); await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server const brewToSave = { - ...currentBrew, - text : currentBrew.text.normalize('NFC'), - pageCount: ((currentBrew.renderer === 'legacy' ? currentBrew.text.match(/\\page/g) : currentBrew.text.match(/^\\page$/gm)) || []).length + 1, - patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(currentBrew.text.normalize('NFC')))), + ...brew, + text : brew.text.normalize('NFC'), + pageCount: ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), hash : await md5(savedBrew.current.text), textBin : undefined }; const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); - const transfer = saveGoogle === _.isNil(currentBrew.googleId); - const params = transfer ? `?${saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; + const transfer = saveToGoogle === _.isNil(brew.googleId); + const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; const res = await request .put(`/api/update/${brewToSave.editId}${params}`) @@ -244,24 +239,17 @@ const EditPage = (props) => { const { googleId, editId, shareId, version } = res.body; savedBrew.current = { - ...currentBrew, + ...brew, googleId: googleId ?? null, editId, shareId, version }; - setCurrentBrew(prev => ({ - ...prev, - googleId: googleId ?? null, - editId, - shareId, - version - })); + setCurrentBrew(savedBrew.current); setIsSaving(false); setUnsavedTime(new Date()); - setUnsavedChanges(!_.isEqual(currentBrew, savedBrew.current)); history.replaceState(null, null, `/edit/${editId}`); }; From 8706f91b586345a8b5e257a2695271dcf0af4c2e Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 08:37:17 -0400 Subject: [PATCH 07/12] Fis autosaveWarning --- client/homebrew/pages/editPage/editPage.jsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index bfb268496..3c4039fad 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -35,7 +35,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; -const SAVE_TIMEOUT = 5000; +const SAVE_TIMEOUT = 10000; const EditPage = (props) => { props = { @@ -58,7 +58,7 @@ const EditPage = (props) => { const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); const [url , setUrl ] = useState(''); const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); - const [autoSaveWarning , setAutoSaveWarning ] = useState(false); + const [autoSaveWarning , setAutoSaveWarning ] = useState(true); const [unsavedTime , setUnsavedTime ] = useState(new Date()); const editorRef = useRef(null); @@ -297,6 +297,7 @@ const EditPage = (props) => { // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING if (unsavedChanges && autoSaveWarning) { resetAutoSaveWarning(); + console.log("just set the timer") const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 ? 'Autosave is OFF.' @@ -323,13 +324,13 @@ const EditPage = (props) => { const toggleAutoSave = () => { if (warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); + setAutoSaveWarning(autoSaveWarning); setAutoSaveEnabled(!autoSaveEnabled); - setAutoSaveWarning(false); }; const resetAutoSaveWarning = () => { - setTimeout(setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds - warningTimer.current = setTimeout(setAutoSaveWarning(true), 900000); // 15 minutes between unsaved changes warnings + setTimeout(()=>setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds + warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 3000); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( From 6f2c3975748c7a67f79c6a4df06d0e407a4a37ed Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 20:47:09 -0400 Subject: [PATCH 08/12] Restore autosave warning to 15 minutes --- client/homebrew/pages/editPage/editPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 3c4039fad..d22aeadb8 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -330,7 +330,7 @@ const EditPage = (props) => { const resetAutoSaveWarning = () => { setTimeout(()=>setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds - warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 3000); // 15 minutes between unsaved changes warnings + warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 90000); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( From 8a0f350c475bf78d41890e7b75afcc1b87a9d21c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:19:43 -0400 Subject: [PATCH 09/12] Fix mutating HTMLErrors directly instead of setState --- client/homebrew/pages/editPage/editPage.jsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d22aeadb8..b748efa87 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -126,9 +126,7 @@ const EditPage = (props) => { const handleTextChange = (text) => { //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) - HTMLErrors = Markdown.validate(text); - - setHTMLErrors(HTMLErrors); + setHTMLErrors(Markdown.validate(text)); setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); }; @@ -139,9 +137,7 @@ const EditPage = (props) => { const handleSnipChange = (snippet)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) - HTMLErrors = Markdown.validate(snippet); - - setHTMLErrors(HTMLErrors); + setHTMLErrors(Markdown.validate(snippet)); setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); }; From 1044aa74b0127c0b49a627205be074d4a365ec9a Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:27:58 -0400 Subject: [PATCH 10/12] Cleanup --- client/homebrew/pages/editPage/editPage.jsx | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index b748efa87..161431663 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -5,10 +5,10 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; -import _ from 'lodash';; -import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; +import _ from 'lodash';; +import {makePatches, stringifyPatches} from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; const { Meta } = require('vitreum/headtags'); @@ -28,14 +28,16 @@ import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import LockNotification from './lockNotification/lockNotification.jsx'; -import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; -import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; +import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; 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 EditPage = (props) => { props = { @@ -77,9 +79,8 @@ const EditPage = (props) => { document.addEventListener('keydown', handleControlKeys); window.onbeforeunload = () => { - if (isSaving || unsavedChanges) { + if (isSaving || unsavedChanges) return 'You have unsaved changes!'; - } }; return () => { @@ -293,7 +294,6 @@ const EditPage = (props) => { // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING if (unsavedChanges && autoSaveWarning) { resetAutoSaveWarning(); - console.log("just set the timer") const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 ? 'Autosave is OFF.' @@ -325,8 +325,8 @@ const EditPage = (props) => { }; const resetAutoSaveWarning = () => { - setTimeout(()=>setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds - warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 90000); // 15 minutes between unsaved changes warnings + setTimeout(()=>setAutoSaveWarning(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds + warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( From 95a1d74644a76b92cc3482e9f860defc34b7868c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:35:55 -0400 Subject: [PATCH 11/12] Linting --- client/homebrew/pages/editPage/editPage.jsx | 141 ++++++++++---------- 1 file changed, 69 insertions(+), 72 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 161431663..925a36e86 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -5,12 +5,11 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; -import _ from 'lodash';; -import {makePatches, stringifyPatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; - -const { Meta } = require('vitreum/headtags'); +import _ from 'lodash';; +import { makePatches, stringifyPatches } from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; +import { Meta } from 'vitreum/headtags'; import Nav from 'naturalcrit/nav/nav.jsx'; import Navbar from '../../navbar/navbar.jsx'; @@ -39,9 +38,9 @@ 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 EditPage = (props) => { +const EditPage = (props)=>{ props = { - brew: DEFAULT_BREW_LOAD, + brew : DEFAULT_BREW_LOAD, ...props }; @@ -64,141 +63,139 @@ const EditPage = (props) => { const [unsavedTime , setUnsavedTime ] = useState(new Date()); const editorRef = useRef(null); - const savedBrew = useRef(_.cloneDeep(props.brew)); - const warningTimer = useRef(null); + const savedBrew = useRef(_.cloneDeep(props.brew)); const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); - useEffect(() => { + useEffect(()=>{ setUrl(window.location.href); const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); setAutoSaveEnabled(autoSavePref); - setAutoSaveWarning(!autoSavePref) + setAutoSaveWarning(!autoSavePref); setHTMLErrors(Markdown.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); document.addEventListener('keydown', handleControlKeys); - window.onbeforeunload = () => { - if (isSaving || unsavedChanges) + window.onbeforeunload = ()=>{ + if(isSaving || unsavedChanges) return 'You have unsaved changes!'; }; - return () => { + return ()=>{ document.removeEventListener('keydown', handleControlKeys); window.onbeforeunload = null; }; }, []); - useEffect(() => { + useEffect(()=>{ const hasChange = !_.isEqual(currentBrew, savedBrew.current); setUnsavedChanges(hasChange); if(hasChange && autoSaveEnabled) trySave(); }, [currentBrew]); - const handleControlKeys = (e) => { - if (!(e.ctrlKey || e.metaKey)) return; + const handleControlKeys = (e)=>{ + if(!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; - if (e.keyCode === S_KEY) trySave(true); - if (e.keyCode === P_KEY) printCurrentBrew(); - if (e.keyCode === S_KEY || e.keyCode === P_KEY) { + if(e.keyCode === S_KEY) trySave(true); + if(e.keyCode === P_KEY) printCurrentBrew(); + if(e.keyCode === S_KEY || e.keyCode === P_KEY) { e.stopPropagation(); e.preventDefault(); } }; - const handleSplitMove = () => { + const handleSplitMove = ()=>{ editorRef.current?.update(); }; - const handleEditorViewPageChange = (pageNumber) => { + const handleEditorViewPageChange = (pageNumber)=>{ setCurrentEditorViewPageNum(pageNumber); }; - const handleEditorCursorPageChange = (pageNumber) => { + const handleEditorCursorPageChange = (pageNumber)=>{ setCurrentEditorCursorPageNum(pageNumber); }; - const handleBrewRendererPageChange = (pageNumber) => { + const handleBrewRendererPageChange = (pageNumber)=>{ setCurrentBrewRendererPageNum(pageNumber); }; - const handleTextChange = (text) => { + const handleTextChange = (text)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) setHTMLErrors(Markdown.validate(text)); - setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); + setCurrentBrew((prevBrew)=>({ ...prevBrew, text })); }; - const handleStyleChange = (style) => { - setCurrentBrew(prevBrew => ({ ...prevBrew, style })); + const handleStyleChange = (style)=>{ + setCurrentBrew((prevBrew)=>({ ...prevBrew, style })); }; const handleSnipChange = (snippet)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) setHTMLErrors(Markdown.validate(snippet)); - setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); + setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet })); }; - const handleMetaChange = (metadata, field = undefined) => { - if (field === 'theme' || field === 'renderer') + const handleMetaChange = (metadata, field = undefined)=>{ + if(field === 'theme' || field === 'renderer') fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - setCurrentBrew(prev => ({ ...prev, ...metadata })); + setCurrentBrew((prev)=>({ ...prev, ...metadata })); }; - const updateBrew = (newData) => - setCurrentBrew((prevBrew) => ({ - ...prevBrew, - style : newData.style, - text : newData.text, - snippets : newData.snippets - })); - - const trySave = (immediate = false) => { + const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ + ...prevBrew, + style : newData.style, + text : newData.text, + snippets : newData.snippets + })); + + const trySave = (immediate = false)=>{ //debounceSave = _.debounce(save, SAVE_TIMEOUT); - if (isSaving) return; + if(isSaving) return; const hasChange = !_.isEqual(currentBrew, savedBrew.current); - if (immediate) { + if(immediate) { debounceSave(currentBrew, saveGoogle); debounceSave.flush?.(); return; } - if (hasChange) + if(hasChange) debounceSave(currentBrew, saveGoogle); else debounceSave.cancel?.(); }; - const handleGoogleClick = () => { - if (!global.account?.googleId) { + const handleGoogleClick = ()=>{ + if(!global.account?.googleId) { setAlertLoginToTransfer(true); return; } - setConfirmGoogleTransfer((prev) => !prev); + setConfirmGoogleTransfer((prev)=>!prev); setError(null); }; - const closeAlerts = (e) => { + const closeAlerts = (e)=>{ e.stopPropagation(); //Only handle click once so alert doesn't reopen setAlertTrashedGoogleBrew(false); setAlertLoginToTransfer(false); setConfirmGoogleTransfer(false); }; - const toggleGoogleStorage = () => { - setSaveGoogle((prev) => !prev); + const toggleGoogleStorage = ()=>{ + setSaveGoogle((prev)=>!prev); setError(null); trySave(true); }; - const save = async (brew, saveToGoogle) => { + const save = async (brew, saveToGoogle)=>{ debounceSave?.cancel?.(); setIsSaving(true); @@ -211,11 +208,11 @@ const EditPage = (props) => { //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$/gm)) || []).length + 1, - patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), - hash : await md5(savedBrew.current.text), - textBin : undefined + text : brew.text.normalize('NFC'), + pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), + hash : await md5(savedBrew.current.text), + textBin : undefined }; const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); @@ -237,7 +234,7 @@ const EditPage = (props) => { savedBrew.current = { ...brew, - googleId: googleId ?? null, + googleId : googleId ?? null, editId, shareId, version @@ -251,7 +248,7 @@ const EditPage = (props) => { history.replaceState(null, null, `/edit/${editId}`); }; - const renderGoogleDriveIcon = () => ( + const renderGoogleDriveIcon = ()=>( Google Drive icon @@ -286,13 +283,13 @@ const EditPage = (props) => { ); - const renderSaveButton = () => { + const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING - if (isSaving) + if(isSaving) return saving...; // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if (unsavedChanges && autoSaveWarning) { + if(unsavedChanges && autoSaveWarning) { resetAutoSaveWarning(); const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 @@ -306,42 +303,42 @@ const EditPage = (props) => { } // #3 - Unsaved changes exist, click to save, show SAVE NOW - if (unsavedChanges) - return trySave(true)} color='blue' icon='fas fa-save'>Save Now + if(unsavedChanges) + return trySave(true)} color='blue' icon='fas fa-save'>Save Now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if (autoSaveEnabled) + if(autoSaveEnabled) return auto-saved.; // DEFAULT - No unsaved changes, show SAVED return saved.; }; - const toggleAutoSave = () => { - if (warningTimer.current) clearTimeout(warningTimer.current); + const toggleAutoSave = ()=>{ + if(warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); setAutoSaveWarning(autoSaveWarning); setAutoSaveEnabled(!autoSaveEnabled); }; - const resetAutoSaveWarning = () => { + const resetAutoSaveWarning = ()=>{ setTimeout(()=>setAutoSaveWarning(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved changes warnings }; - const renderAutoSaveButton = () => ( + const renderAutoSaveButton = ()=>( Autosave ); - const processShareId = () => ( + const processShareId = ()=>( currentBrew.googleId && !currentBrew.stubbed ? currentBrew.googleId + currentBrew.shareId : currentBrew.shareId ); - const getRedditLink = () => { + const getRedditLink = ()=>{ const shareLink = processShareId(); const systems = currentBrew.systems.length > 0 ? ` [${currentBrew.systems.join(' - ')}]` : ''; const title = `${currentBrew.title} ${systems}`; @@ -352,7 +349,7 @@ const EditPage = (props) => { return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; }; - const clearError = () => { + const clearError = ()=>{ setError(null); setIsSaving(false); }; From c2e6150edfe10ff00c274bed31f26d8403a15dde Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:39:11 -0400 Subject: [PATCH 12/12] Fix mistaken delete --- client/homebrew/pages/editPage/editPage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 925a36e86..49512d66c 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -64,6 +64,7 @@ const EditPage = (props)=>{ const editorRef = useRef(null); const savedBrew = useRef(_.cloneDeep(props.brew)); + const warningTimer = useRef(null); const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); useEffect(()=>{