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
- ;
+ ;
}
});