diff --git a/client/homebrew/pages/basePages/editPage/editPage.jsx b/client/homebrew/pages/basePages/editPage/editPage.jsx index 082b89896..e7b0149f4 100644 --- a/client/homebrew/pages/basePages/editPage/editPage.jsx +++ b/client/homebrew/pages/basePages/editPage/editPage.jsx @@ -1,5 +1,6 @@ require('./editPage.less'); const React = require('react'); +const _ = require('lodash'); const Nav = require('naturalcrit/nav/nav.jsx'); const Navbar = require('../../../navbar/navbar.jsx'); @@ -25,9 +26,13 @@ const STYLEKEY = 'homebrewery-new-style'; const METAKEY = 'homebrewery-new-meta'; const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; +const SAVE_TIMEOUT = 10000; + const BaseEditPage = (props)=>{ const [brew, setBrew] = useState(() => props.brew); + const [savedBrew, setSavedBrew] = useState(brew); const [isSaving, setIsSaving] = useState(false); + const [lastSavedTime, setLastSavedTime] = useState(new Date()); const [saveGoogle, setSaveGoogle] = useState(() => (global.account?.googleId ? true : false)); const [welcomeText, setWelcomeText] = useState(() => props.brew?.text ?? ''); const [error, setError] = useState(undefined); @@ -36,8 +41,14 @@ const BaseEditPage = (props)=>{ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle, setThemeBundle] = useState({}); + const [unsavedChanges, setUnsavedChanges] = useState(false); + const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); + const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(false); - const editorRef = useRef(null); + const editorRef = useRef(null); + let lastSavedBrew = useRef(JSON.parse(JSON.stringify(this.propcopys.brew))); //Deep copy + const saveTimeout = useRef(null); + const unsavedChangesTimer = useRef(null); const handleSplitMove = ()=>{ editorRef.current.update(); @@ -75,6 +86,11 @@ const BaseEditPage = (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(text); + + setHTMLErrors(htmlErrors); setBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); }; @@ -91,19 +107,39 @@ const BaseEditPage = (props)=>{ 'lang' : metadata.lang })); }; - + + const updateBrew = (newData) => { + setBrew(prevBrew => ({ //TODO: May be able to just directly use setBrew instead of a wrapper, if its safe to assume we want all the data from newData + ...prevBrew, //OR: Somehow combine handleTextChange, handleStyleChange, handleMetaChange, and handleSnipChange into one function that calls this + style: newData.style, + text: newData.text, + snippets: newData.snippets + })); + }; + const clearError = ()=>{ setError(null); setIsSaving(false); }; - const save = async ()=>{ - setIsSaving(true); - await props.performSave(brew, saveGoogle) - .catch((err)=>{ - setError(err); - }); - setIsSaving(false) + const save = async (immediate=false)=>{ + if(isSaving) return; + if(!unsavedChanges && !immediate) return; + + clearTimeout(saveTimeout.current); + + const timeout = immediate ? 0 : 10000; + + saveTimeout.current = setTimeout(async () => { + setIsSaving(true); + await props.performSave(brew, saveGoogle) + .catch((err)=>{ + setError(err); + }); + setIsSaving(false); + setLastSavedTime(new Date()); + setTimeout(setWarnUnsavedChanges(true), 900000); // 15 minutes between unsaved work warnings + }, timeout); }; const handleControlKeys = (e)=>{ @@ -118,17 +154,89 @@ const BaseEditPage = (props)=>{ } }; + const hasChanges =()=>{ + return !_.isEqual(brew, savedBrew); + }; + useEffect(() => { props.loadBrew?.(brew, setBrew, setSaveGoogle); //Initial load from localStorage/etc. + //Load settings + setAutoSaveEnabled(JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true); + + setHTMLErrors(Markdown.validate(brew.text)); + document.addEventListener('keydown', handleControlKeys); - return document.removeEventListener('keydown', handleControlKeys); + window.onbeforeunload = ()=>{ + if(isSaving || unsavedChanges) + return 'You have unsaved changes!'; + }; + + return () => { + document.removeEventListener('keydown', handleControlKeys); + window.onbeforeunload = null; + } }, []); useEffect(() => { fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme); }, [brew.renderer, brew.theme]); + useEffect(() => { + const hasChange = hasChanges(); + if(unsavedChanges !== hasChange) + setUnsavedChanges(hasChange); + + if(autoSaveEnabled) save(); + }, [brew]); + + const resetUnsavedChangesWarning = ()=>{ + setTimeout(setWarnUnsavedChanges(false), 4000); // Display warning for 4 seconds + setTimeout(setWarnUnsavedChanges(true) , 90000); // 15 minutes between warnings + }; + + const toggleAutoSave = ()=>{ + if(unsavedChangesTimer.current) clearTimeout(unsavedChangesTimer.current); + localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); + setAutoSaveEnabled(!autoSaveEnabled); + setWarnUnsavedChanges(false); + }; + + 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){ + resetUnsavedChangesWarning(); + 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.`; + + return + Reminder... +
{text}
+
; + } + + // #3 - Unsaved changes exist, click to save, show SAVE NOW + if(unsavedChanges) + return save(true)} color='blue' icon='fas fa-save'>Save Now; + + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED + if(autoSaveEnabled) + return auto-saved.; + + // DEFAULT - No unsaved changes, show SAVED + return saved.; + }; + + const renderAutoSaveButton = ()=>{ + return + Autosave + ; + }; + return (
@@ -138,7 +246,10 @@ const BaseEditPage = (props)=>{ {error ? - : props.saveButton?.(save, isSaving) + : + {renderSaveButton()} + {renderAutoSaveButton()} + } {props.renderUniqueNav?.()} @@ -160,10 +271,13 @@ const BaseEditPage = (props)=>{ onTextChange={handleTextChange} onStyleChange={handleStyleChange} onMetaChange={handleMetaChange} + onSnipChange={handleSnipChange} + reportError={this.errorReported} renderer={brew.renderer} showEditButtons={false} //FALSE FOR HOME PAGE userThemes={props.userThemes} themeBundle={themeBundle} + updateBrew={updateBrew} onCursorPageChange={handleEditorCursorPageChange} onViewPageChange={handleEditorViewPageChange} currentEditorViewPageNum={currentEditorViewPageNum} @@ -187,7 +301,7 @@ const BaseEditPage = (props)=>{
- {props.children?.(welcomeText, brew.text, save)} + {props.children?.(welcomeText, brew, save)} ); }; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 57c3af1b0..420190589 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -12,12 +12,7 @@ const { Meta } = require('vitreum/headtags'); const Nav = require('naturalcrit/nav/nav.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); - const BaseEditPage = require('../basePages/editPage/editPage.jsx'); -const SplitPane = require('client/components/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const LockNotification = require('./lockNotification/lockNotification.jsx'); @@ -30,7 +25,6 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers const googleDriveIcon = require('../../googleDrive.svg'); -const SAVE_TIMEOUT = 10000; const EditPage = createClass({ displayName : 'EditPage', @@ -42,171 +36,17 @@ const EditPage = createClass({ 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 : {} }; }, - editor : React.createRef(null), - savedBrew : null, - componentDidMount : function(){ - this.setState({ - url : window.location.href - }); - this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy - - 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){ - return 'You have unsaved changes!'; - } - }; - - this.setState((prevState)=>({ - htmlErrors : Markdown.validate(prevState.brew.text) - })); - - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); - - 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 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){ - e.stopPropagation(); - e.preventDefault(); - } - }, - - handleSplitMove : function(){ - this.editor.current.update(); - }, - - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, - - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, - - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: 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); - - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, - - 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); - - this.setState((prevState)=>({ - brew : { ...prevState.brew, snippets: snippet }, - unsavedChanges : true, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, - - handleStyleChange : function(style){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, - - handleMetaChange : function(metadata, field=undefined){ - if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle(this, metadata.renderer, metadata.theme); - - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - ...metadata - } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, - - 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 - } - })); - }, - - 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(); }, handleGoogleClick : function(){ @@ -217,11 +57,10 @@ const EditPage = createClass({ return; } this.setState((prevState)=>({ - confirmGoogleTransfer : !prevState.confirmGoogleTransfer + confirmGoogleTransfer : !prevState.confirmGoogleTransfer, + error : null })); - this.setState({ - error : null - }); + }, closeAlerts : function(event){ @@ -297,7 +136,6 @@ const EditPage = createClass({ version : res.body.version }, isSaving : false, - unsavedTime : new Date() }), ()=>{ this.setState({ unsavedChanges : this.hasChanges() }); }); @@ -330,7 +168,7 @@ const EditPage = createClass({ You must be signed in to a Google account to transfer between the homebrewery and Google Drive! + href={`https://www.naturalcrit.com/login?redirect=${window.Location.href}`}>
Sign In
@@ -352,68 +190,6 @@ const EditPage = createClass({ ; }, - renderSaveButton : function(){ - - // #1 - Currently saving, show SAVING - if(this.state.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.`; - - return - 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; - } - // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(this.state.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)); - }); - }, - - 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; - }, - - errorReported : function(error) { - this.setState({ - error - }); - }, - - renderAutoSaveButton : function(){ - return - Autosave - ; - }, - processShareId : function() { return this.state.brew.googleId && !this.state.brew.stubbed ? this.state.brew.googleId + this.state.brew.shareId : @@ -421,7 +197,6 @@ const EditPage = createClass({ }, 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}`; @@ -438,13 +213,6 @@ const EditPage = createClass({ return <> {this.renderGoogleDriveIcon()} - {this.state.error ? - : - - {this.renderSaveButton()} - {this.renderAutoSaveButton()} - - } share @@ -464,52 +232,21 @@ const EditPage = createClass({ }, render : function(){ - return - - {this.props.brew.lock && } -
- - - - -
-
; + {(welcomeText, brew, save) => { + return <> + + {this.props.brew.lock && } + + }} + ; } }); diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index a38267617..d0261fecc 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -30,25 +30,24 @@ const HomePage = createClass({ }, render : function(){ - return - {(welcomeText, brewText, save) => { - return <> - -
- Save current -
+ performSave={this.save}> + {(welcomeText, brew, save) => { + return <> + +
+ Save current +
-
- Create your own - - - }} - + + Create your own + + + }} + } }); diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 7647ec20b..8f59b7593 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -66,27 +66,14 @@ const NewPage = createClass({ }); }, - renderSaveButton : function(save, isSaving){ - if(isSaving){ - return - save... - ; - } else { - return - save - ; - } - }, - render : function(){ - return - ; + ; } });