diff --git a/client/homebrew/navbar/share.navitem.jsx b/client/homebrew/navbar/share.navitem.jsx
new file mode 100644
index 000000000..a08ac6878
--- /dev/null
+++ b/client/homebrew/navbar/share.navitem.jsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import dedent from 'dedent-tabs';
+import Nav from 'naturalcrit/nav/nav.jsx';
+
+ const getShareId = (brew)=>(
+ brew.googleId && !brew.stubbed
+ ? brew.googleId + brew.shareId
+ : brew.shareId
+ );
+
+ const getRedditLink = (brew)=>{
+ const text = dedent`
+ Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
+
+ **[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
+
+ return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
+ };
+
+export default ({brew}) => (
+
+
+ share
+
+
+ view
+
+ {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
+ copy url
+
+
+ post to reddit
+
+
+);
diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx
index 715ae72f9..af39838cf 100644
--- a/client/homebrew/pages/editPage/editPage.jsx
+++ b/client/homebrew/pages/editPage/editPage.jsx
@@ -15,6 +15,7 @@ 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 ShareNavItem from '../../navbar/share.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
@@ -48,6 +49,7 @@ const EditPage = (props)=>{
const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false);
+ const [lastSavedTime , setLastSavedTime ] = useState(new Date());
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
@@ -59,56 +61,56 @@ const EditPage = (props)=>{
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
- const [url , setUrl ] = useState('');
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
- const [autoSaveWarning , setAutoSaveWarning ] = useState(true);
- const [unsavedTime , setUnsavedTime ] = useState(new Date());
+ const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true);
- 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), []);
+ const editorRef = useRef(null);
+ const lastSavedBrew = useRef(_.cloneDeep(props.brew));
+ const saveTimeout = useRef(null);
+ const warnUnsavedTimeout = useRef(null);
+ const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
+ const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
- setUrl(window.location.href);
-
const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true);
setAutoSaveEnabled(autoSavePref);
- setAutoSaveWarning(!autoSavePref);
+ setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(Markdown.validate(currentBrew.text));
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
- document.addEventListener('keydown', handleControlKeys);
- window.onbeforeunload = ()=>{
- if(isSaving || unsavedChanges)
- return 'You have unsaved changes!';
+ const handleControlKeys = (e)=>{
+ if(!(e.ctrlKey || e.metaKey)) return;
+ if(e.keyCode === 83) trySaveRef.current(true);
+ if(e.keyCode === 80) printCurrentBrew();
+ if([83, 80].includes(e.keyCode)) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
};
+ document.addEventListener('keydown', handleControlKeys);
+ window.onbeforeunload = ()=>{
+ if(unsavedChangesRef.current)
+ return 'You have unsaved changes!';
+ };
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
- window.onbeforeunload = null;
+ window.onBeforeUnload = null;
};
}, []);
useEffect(()=>{
- const hasChange = !_.isEqual(currentBrew, savedBrew.current);
+ trySaveRef.current = trySave;
+ unsavedChangesRef.current = unsavedChanges;
+ });
+
+ useEffect(()=>{
+ const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
- if(hasChange && autoSaveEnabled) trySave();
+ if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
- 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) {
- e.stopPropagation();
- e.preventDefault();
- }
- };
-
const handleSplitMove = ()=>{
editorRef.current?.update();
};
@@ -143,22 +145,10 @@ const EditPage = (props)=>{
snippets : newData.snippets
}));
- const trySave = (immediate = false)=>{
- //debounceSave = _.debounce(save, SAVE_TIMEOUT);
- if(isSaving) return;
-
- const hasChange = !_.isEqual(currentBrew, savedBrew.current);
-
- if(immediate) {
- debounceSave(currentBrew, saveGoogle);
- debounceSave.flush?.();
- return;
- }
-
- if(hasChange)
- debounceSave(currentBrew, saveGoogle);
- else
- debounceSave.cancel?.();
+ const resetWarnUnsavedTimer = ()=>{
+ setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
+ clearTimeout(warnUnsavedTimeout.current);
+ warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
};
const handleGoogleClick = ()=>{
@@ -184,11 +174,26 @@ const EditPage = (props)=>{
trySave(true);
};
- const save = async (brew, saveToGoogle)=>{
- debounceSave?.cancel?.();
+ const trySave = (immediate = false, hasChanges = true)=>{
+ clearTimeout(saveTimeout.current);
+ if(isSaving) return;
+ if(!hasChanges && !immediate) return;
+ const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
- setIsSaving(true);
- setError(null);
+ saveTimeout.current = setTimeout(async ()=>{
+ setIsSaving(true);
+ setError(null);
+ await save(currentBrew, saveGoogle)
+ .catch((err)=>{
+ setError(err);
+ });
+ setIsSaving(false);
+ setLastSavedTime(new Date());
+ if(!autoSaveEnabled) resetWarnUnsavedTimer();
+ }, newTimeout);
+ };
+
+ const save = async (brew, saveToGoogle)=>{
setHTMLErrors(Markdown.validate(brew.text));
await updateHistory(brew).catch(console.error);
@@ -199,9 +204,10 @@ const EditPage = (props)=>{
...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
+ patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
+ hash : await md5(lastSavedBrew.current.text),
+ textBin : undefined,
+ version : lastSavedBrew.current.version
};
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave)));
@@ -219,22 +225,24 @@ const EditPage = (props)=>{
});
if(!res) return;
- const { googleId, editId, shareId, version } = res.body;
-
- savedBrew.current = {
- ...brew,
- googleId : googleId ?? null,
- editId,
- shareId,
- version
+ const updatedFields = {
+ googleId : res.body.googleId ?? null,
+ editId : res.body.editId,
+ shareId : res.body.shareId,
+ version : res.body.version
};
- setCurrentBrew(savedBrew.current);
+ lastSavedBrew.current = {
+ ...brew,
+ ...updatedFields
+ };
- setIsSaving(false);
- setUnsavedTime(new Date());
+ setCurrentBrew((prevBrew)=>({
+ ...prevBrew,
+ ...updatedFields
+ }));
- history.replaceState(null, null, `/edit/${editId}`);
+ history.replaceState(null, null, `/edit/${res.body.editId}`);
};
const renderGoogleDriveIcon = ()=>(
@@ -255,7 +263,7 @@ const EditPage = (props)=>{
{alertLoginToTransfer && (
You must be signed in to a Google account to transfer between the homebrewery and Google Drive!
-
+
Sign In
Not Now
@@ -278,17 +286,17 @@ const EditPage = (props)=>{
return
saving...;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
- if(unsavedChanges && autoSaveWarning) {
- resetAutoSaveWarning();
- const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60);
+ if(unsavedChanges && warnUnsavedChanges) {
+ resetWarnUnsavedTimer();
+ const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
const text = elapsedTime === 0
? 'Autosave is OFF.'
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return
Reminder...
- {text}
-
+
{text}
+ ;
}
// #3 - Unsaved changes exist, click to save, show SAVE NOW
@@ -304,15 +312,11 @@ const EditPage = (props)=>{
};
const toggleAutoSave = ()=>{
- if(warningTimer.current) clearTimeout(warningTimer.current);
+ clearTimeout(warnUnsavedTimeout.current);
+ clearTimeout(saveTimeout.current);
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled));
- setAutoSaveWarning(autoSaveWarning);
setAutoSaveEnabled(!autoSaveEnabled);
- };
-
- 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
+ setWarnUnsavedChanges(autoSaveEnabled);
};
const renderAutoSaveButton = ()=>(
@@ -321,31 +325,12 @@ const EditPage = (props)=>{
);
- const processShareId = ()=>(
- currentBrew.googleId && !currentBrew.stubbed
- ? currentBrew.googleId + currentBrew.shareId
- : currentBrew.shareId
- );
-
- 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})**`;
-
- return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
- };
-
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = ()=>{
- const shareLink = processShareId();
-
return
{currentBrew.title}
@@ -356,25 +341,12 @@ const EditPage = (props)=>{
{error
?
:
- {renderSaveButton()}
- {renderAutoSaveButton()}
- }
+ {renderSaveButton()}
+ {renderAutoSaveButton()}
+ }
-
-
- share
-
-
- view
-
- {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
- copy url
-
-
- post to reddit
-
-
+