Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fit-listpage-into-vault

This commit is contained in:
Víctor Losada Hernández
2026-09-10 11:13:22 +02:00
7 changed files with 220 additions and 170 deletions
-12
View File
@@ -9,22 +9,10 @@ const Navbar = createReactClass({
displayName : 'Navbar', displayName : 'Navbar',
getInitialState : function() { getInitialState : function() {
return { return {
// showNonChromeWarning: false, // uncomment if needed
ver : global.version || '0.0.0' ver : global.version || '0.0.0'
}; };
}, },
/*
renderChromeWarning : function(){
if(!this.state.showNonChromeWarning) return;
return <Nav.item className='warning' icon='fa-exclamation-triangle'>
Optimized for Chrome
<div className='dropdown'>
If you are experiencing rendering issues, use Chrome instead
</div>
</Nav.item>
},
*/
render : function(){ render : function(){
return <Nav.base> return <Nav.base>
<Nav.section> <Nav.section>
+43 -47
View File
@@ -10,18 +10,20 @@ import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js'
import SplitPane from '@components/splitPane/splitPane.jsx'; import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '@navbar/nav.jsx'; import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx'; import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx'; import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx'; import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx'; import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx'; import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx'; import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx'; import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems; 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_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
const BREWKEY = 'HB_newPage_content'; const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style'; const STYLEKEY = 'HB_newPage_style';
@@ -49,7 +50,7 @@ const SNIPKEY = 'HB_newPage_snippets';
const METAKEY = 'HB_newPage_meta'; const METAKEY = 'HB_newPage_meta';
const useLocalStorage = false; const useLocalStorage = false;
const neverSaved = false; const sandbox = false;
const EditPage = (props)=>{ const EditPage = (props)=>{
props = { props = {
@@ -82,8 +83,26 @@ 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 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 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(()=>{ useEffect(()=>{
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true); const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
setAutoSaveEnabled(autoSavePref); setAutoSaveEnabled(autoSavePref);
setWarnUnsavedChanges(!autoSavePref); setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(hbfm.validate(currentBrew.text)); setHTMLErrors(hbfm.validate(currentBrew.text));
@@ -104,6 +123,7 @@ const EditPage = (props)=>{
if(unsavedChangesRef.current) if(unsavedChangesRef.current)
return 'You have unsaved changes!'; return 'You have unsaved changes!';
}; };
return ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
window.onbeforeunload = null; window.onbeforeunload = null;
@@ -126,29 +146,6 @@ const EditPage = (props)=>{
editorRef.current?.update(); 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)=>({ const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
...prevBrew, ...prevBrew,
style : newData.style, style : newData.style,
@@ -163,8 +160,8 @@ const EditPage = (props)=>{
}; };
const handleGoogleClick = ()=>{ const handleGoogleClick = ()=>{
if(global.account !== currentBrew.authors[0]) { if(currentBrew.authors.length > 0 && global.account?.username !== currentBrew.authors[0]) {
setalertOwnershipToTransfer(true); setAlertOwnershipToTransfer(true);
return; return;
} }
if(!global.account?.googleId) { if(!global.account?.googleId) {
@@ -181,7 +178,7 @@ const EditPage = (props)=>{
setAlertTrashedGoogleBrew(false); setAlertTrashedGoogleBrew(false);
setAlertNoGoogleToTransfer(false); setAlertNoGoogleToTransfer(false);
setConfirmGoogleTransfer(false); setConfirmGoogleTransfer(false);
setalertOwnershipToTransfer(false); setAlertOwnershipToTransfer(false);
}; };
const toggleGoogleStorage = (e)=>{ const toggleGoogleStorage = (e)=>{
@@ -267,12 +264,11 @@ const EditPage = (props)=>{
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}> <Nav.item className='googleDriveStorage' onClick={handleGoogleClick}>
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' /> <img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' />
{alertOwnershipToTransfer && ( {alertOwnershipToTransfer && (
<div className='errorContainer'> <div className='errorContainer'>
You must be the Owner to transfer between the Homebrewery and Google Drive! You must be the Owner to transfer between the Homebrewery and Google Drive!
The owner of this file is {currentBrew.authors[0]} The owner of this file is {currentBrew.authors[0]}.
<br></br>
<div className='confirm' onClick={closeAlerts}> Okay </div> <div className='confirm' onClick={closeAlerts}> Okay </div>
</div> </div>
)} )}
@@ -318,12 +314,12 @@ const EditPage = (props)=>{
resetWarnUnsavedTimer(); resetWarnUnsavedTimer();
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
const text = elapsedTime === 0 const text = elapsedTime === 0
? 'Autosave is OFF.' ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.`
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'> return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder... Reminder...
<div className='errorContainer'>{text}</div> <div className='errorContainer'>{text}</div>
</Nav.item>; </Nav.item>;
} }
@@ -335,9 +331,9 @@ const EditPage = (props)=>{
if(autoSaveEnabled) if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>; return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button
if(neverSaved) if(sandbox)
return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>; return <Nav.item className='save sandbox' disabled={true}>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED // DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>; return <Nav.item className='save saved'>saved</Nav.item>;
+64 -49
View File
@@ -10,18 +10,20 @@ import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js'
import SplitPane from '@components/splitPane/splitPane.jsx'; import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '@navbar/nav.jsx'; import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx'; import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx'; import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx'; import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx'; import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx'; import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx'; import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx'; import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems; const { both: RecentNavItem } = RecentNavItems;
@@ -30,18 +32,22 @@ const { both: RecentNavItem } = RecentNavItems;
import Headtags from '@vitreum/headtags.js'; import Headtags from '@vitreum/headtags.js';
const Meta = Headtags.Meta; const Meta = Headtags.Meta;
const BREWKEY = 'homebrewery-new'; const SAVE_TIMEOUT = 10000;
const STYLEKEY = 'homebrewery-new-style'; const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
const SNIPKEY = 'homebrewery-new-snippets'; const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
const METAKEY = 'homebrewery-new-meta';
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 useLocalStorage = false;
const neverSaved = true; const sandbox = true;
const HomePage =(props)=>{ const HomePage =(props)=>{
props = { props = {
brew : DEFAULT_BREW, brew : DEFAULT_BREW,
ver : '0.0.0',
...props ...props
}; };
@@ -54,13 +60,38 @@ const HomePage =(props)=>{
const [themeBundle, setThemeBundle] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [autoSaveEnabled, setAutoSaveEnable] = useState(false); const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const warnUnsavedTimeout = useRef(null);
const unsavedChangesRef = useRef(unsavedChanges); const unsavedChangesRef = useRef(unsavedChanges);
const {
handleBrewChange
} = useCommonEditPageFunctions({
setError,
setThemeBundle,
HTMLErrors,
setHTMLErrors,
setCurrentBrew,
useLocalStorage,
BREWKEY,
STYLEKEY,
SNIPKEY,
METAKEY,
fetchThemeBundle,
hbfm
});
useEffect(()=>{ 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); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{ const handleControlKeys = (e)=>{
@@ -78,6 +109,7 @@ const HomePage =(props)=>{
if(unsavedChangesRef.current) if(unsavedChangesRef.current)
return 'You have unsaved changes!'; return 'You have unsaved changes!';
}; };
return ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
window.onbeforeunload = null; window.onbeforeunload = null;
@@ -112,27 +144,10 @@ const HomePage =(props)=>{
editorRef.current.update(); editorRef.current.update();
}; };
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' const resetWarnUnsavedTimer = ()=>{
if(subfield == 'renderer' || subfield == 'theme') setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); clearTimeout(warnUnsavedTimeout.current);
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
//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 = ()=>{ const renderSaveButton = ()=>{
@@ -141,18 +156,18 @@ const HomePage =(props)=>{
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>; return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
// if(unsavedChanges && warnUnsavedChanges) { if(unsavedChanges && warnUnsavedChanges) {
// resetWarnUnsavedTimer(); resetWarnUnsavedTimer();
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
// const text = elapsedTime === 0 const text = elapsedTime === 0
// ? 'Autosave is OFF.' ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.`
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'> return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder... Reminder...
// <div className='errorContainer'>{text}</div> <div className='errorContainer'>{text}</div>
// </Nav.item>; </Nav.item>;
// } }
// #3 - Unsaved changes exist, click to save, show SAVE NOW // #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges) if(unsavedChanges)
@@ -162,8 +177,8 @@ const HomePage =(props)=>{
if(autoSaveEnabled) if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>; return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button
if(neverSaved) if(sandbox)
return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>; return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED // DEFAULT - No unsaved changes, show SAVED
+68 -45
View File
@@ -10,31 +10,38 @@ import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js'
import SplitPane from '@components/splitPane/splitPane.jsx'; import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '@navbar/nav.jsx'; import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx'; import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx'; import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx'; import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx'; import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx'; import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx'; import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx'; import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems; const { both: RecentNavItem } = RecentNavItems;
// Page specific imports // 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 AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
const BREWKEY = 'HB_newPage_content'; const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style'; const STYLEKEY = 'HB_newPage_style';
const METAKEY = 'HB_newPage_metadata';
const SNIPKEY = 'HB_newPage_snippets'; const SNIPKEY = 'HB_newPage_snippets';
const METAKEY = 'HB_newPage_meta';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_'; const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true; const useLocalStorage = true;
const neverSaved = true; const sandbox = true;
const NewPage = (props)=>{ const NewPage = (props)=>{
props = { props = {
@@ -44,6 +51,7 @@ const NewPage = (props)=>{
const [currentBrew, setCurrentBrew] = useState(props.brew); const [currentBrew, setCurrentBrew] = useState(props.brew);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false); const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text)); const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text));
@@ -53,16 +61,42 @@ const NewPage = (props)=>{
const [themeBundle, setThemeBundle] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));
// const saveTimeout = useRef(null); // 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 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 unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{ useEffect(()=>{
loadBrew(); loadBrew();
}, []);
const {
handleBrewChange
} = useCommonEditPageFunctions({
setError,
setThemeBundle,
HTMLErrors,
setHTMLErrors,
setCurrentBrew,
useLocalStorage,
BREWKEY,
STYLEKEY,
SNIPKEY,
METAKEY,
fetchThemeBundle,
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); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{ const handleControlKeys = (e)=>{
@@ -76,9 +110,14 @@ const NewPage = (props)=>{
}; };
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
window.onbeforeunload = ()=>{
if(unsavedChangesRef.current)
return 'You have unsaved changes!';
};
return ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
window.onbeforeunload = null;
}; };
}, []); }, []);
@@ -127,27 +166,10 @@ const NewPage = (props)=>{
editorRef.current.update(); editorRef.current.update();
}; };
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata' const resetWarnUnsavedTimer = ()=>{
if(subfield == 'renderer' || subfield == 'theme') setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); clearTimeout(warnUnsavedTimeout.current);
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
//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 ()=>{ const trySave = async ()=>{
@@ -175,6 +197,7 @@ const NewPage = (props)=>{
localStorage.removeItem(BREWKEY); localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY); localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY); localStorage.removeItem(METAKEY);
window.onbeforeunload = null;
window.location = `/edit/${savedBrew.editId}`; window.location = `/edit/${savedBrew.editId}`;
}; };
@@ -184,18 +207,18 @@ const NewPage = (props)=>{
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>; return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
// if(unsavedChanges && warnUnsavedChanges) { if(unsavedChanges && warnUnsavedChanges) {
// resetWarnUnsavedTimer(); resetWarnUnsavedTimer();
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
// const text = elapsedTime === 0 const text = elapsedTime === 0
// ? 'Autosave is OFF.' ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.`
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'> return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder... Reminder...
// <div className='errorContainer'>{text}</div> <div className='errorContainer'>{text}</div>
// </Nav.item>; </Nav.item>;
// } }
// #3 - Unsaved changes exist, click to save, show SAVE NOW // #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges) if(unsavedChanges)
@@ -205,8 +228,8 @@ const NewPage = (props)=>{
if(autoSaveEnabled) if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>; return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button
if(neverSaved) if(sandbox)
return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>; return <Nav.item className='save neverSaved' disabled={true}>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED // DEFAULT - No unsaved changes, show SAVED
@@ -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
}
}
-15
View File
@@ -12085,21 +12085,6 @@
"url": "https://opencollective.com/mongoose" "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": { "node_modules/mongoose/node_modules/mongodb": {
"version": "7.5.0", "version": "7.5.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.5.0.tgz",
+1 -1
View File
@@ -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; if(!renderer || !theme) return;
const res = await request const res = await request
.get(`/api/theme/${renderer}/${theme}`) .get(`/api/theme/${renderer}/${theme}`)