0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-27 03:03:09 +00:00

Compare commits

...

14 Commits

Author SHA1 Message Date
Trevor Buckner
1c6a39363c Combine handleText/Style/Snippet/Meta functions into common function
Also adds any related imports and key names
2025-10-02 19:33:15 -04:00
Trevor Buckner
bcca5fa97d In /homepage, rename brew state to currentBrew to match /new and /edit 2025-10-02 19:27:45 -04:00
Trevor Buckner
bfe6142b04 Merge pull request #4438 from G-Ambatte/fixDefaultSaveLocation-#4437
Fix default save location functionality
2025-10-02 18:37:59 -04:00
Víctor Losada Hernández
aef835dfe7 Merge branch 'master' into fixDefaultSaveLocation-#4437 2025-10-02 12:42:09 +02:00
Víctor Losada Hernández
274fbcb29e Merge pull request #4435 from naturalcrit/remove-scrollbar-styles
remove custom scrollbar styles
2025-10-02 12:40:59 +02:00
G.Ambatte
900cf6aebb Change SAVEKEY definition to after username is populated 2025-10-02 22:59:24 +13:00
Víctor Losada Hernández
6d4ad6af7d Merge branch 'master' of https://github.com/naturalcrit/homebrewery into remove-scrollbar-styles 2025-10-01 22:57:53 +02:00
Víctor Losada Hernández
4b753970c9 remove scrollbar 2025-09-29 22:19:19 +02:00
Trevor Buckner
fb75bd46d0 Merge pull request #4425 from naturalcrit/ChangeAutoSaveToTimer
Fix Autosave and unsaved changes warning
2025-09-22 19:57:02 -04:00
Trevor Buckner
c5071aa27e Restore unsaved warning timeout duration to 15 mins 2025-09-22 19:55:39 -04:00
Trevor Buckner
f0baa763ec lint 2025-09-22 19:52:42 -04:00
Trevor Buckner
3ec650557e Fix Autosave and unsaved changes warning
Use normal setTimeout for autosave instead of _.debounce. Fixes a lot of issues with functional component.

Also fix existing bug where multiple "unsaved data" warnings could be queued up if the user keeps typing while the warning is being displayed.
2025-09-22 19:49:57 -04:00
Trevor Buckner
242ff8712f Merge pull request #4420 from naturalcrit/MoveShareDropdownMenuToSeparateComponent
Move "share" dropdown to own component
2025-09-18 22:48:08 -04:00
Trevor Buckner
31a8101df7 Move "share" dropdown to own component 2025-09-13 19:37:59 -04:00
6 changed files with 216 additions and 203 deletions

View File

@@ -40,11 +40,8 @@ const Editor = createClass({
style : '' style : ''
}, },
onTextChange : ()=>{}, onBrewChange : ()=>{},
onStyleChange : ()=>{}, reportError : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{},
onCursorPageChange : ()=>{}, onCursorPageChange : ()=>{},
onViewPageChange : ()=>{}, onViewPageChange : ()=>{},
@@ -438,7 +435,7 @@ const Editor = createClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onTextChange} onChange={this.props.onBrewChange('text')}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} /> style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
@@ -451,7 +448,7 @@ const Editor = createClass({
language='css' language='css'
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange} onChange={this.props.onBrewChange('style')}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
@@ -467,7 +464,7 @@ const Editor = createClass({
<MetadataEditor <MetadataEditor
metadata={this.props.brew} metadata={this.props.brew}
themeBundle={this.props.themeBundle} themeBundle={this.props.themeBundle}
onChange={this.props.onMetaChange} onChange={this.props.onBrewChange('metadata')}
reportError={this.props.reportError} reportError={this.props.reportError}
userThemes={this.props.userThemes}/> userThemes={this.props.userThemes}/>
</>; </>;
@@ -481,7 +478,7 @@ const Editor = createClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.snippets} value={this.props.brew.snippets}
onChange={this.props.onSnipChange} onChange={this.props.onBrewChange('snippets')}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}

View File

@@ -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}) => (
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item>
<Nav.item color='blue' href={`/share/${getShareId(brew)}`}>
view
</Nav.item>
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
copy url
</Nav.item>
<Nav.item color='blue' href={getRedditLink(brew)} newTab rel='noopener noreferrer'>
post to reddit
</Nav.item>
</Nav.dropdown>
);

View File

@@ -15,6 +15,7 @@ import Nav from 'naturalcrit/nav/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 ShareNavItem from '../../navbar/share.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';
@@ -38,6 +39,11 @@ 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 BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const EditPage = (props)=>{ const EditPage = (props)=>{
props = { props = {
brew : DEFAULT_BREW_LOAD, brew : DEFAULT_BREW_LOAD,
@@ -46,6 +52,7 @@ const EditPage = (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(!!props.brew.googleId); const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId);
const [error , setError ] = useState(null); const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
@@ -57,56 +64,58 @@ const EditPage = (props)=>{
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed); const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
const [url , setUrl ] = useState('');
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
const [autoSaveWarning , setAutoSaveWarning ] = useState(true); const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true);
const [unsavedTime , setUnsavedTime ] = useState(new Date());
const editorRef = useRef(null); const editorRef = useRef(null);
const savedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const warningTimer = useRef(null); const saveTimeout = useRef(null);
const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); 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
const useLocalStorage = false;
useEffect(()=>{ useEffect(()=>{
setUrl(window.location.href);
const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true);
setAutoSaveEnabled(autoSavePref); setAutoSaveEnabled(autoSavePref);
setAutoSaveWarning(!autoSavePref); setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(Markdown.validate(currentBrew.text)); setHTMLErrors(Markdown.validate(currentBrew.text));
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
document.addEventListener('keydown', handleControlKeys); const handleControlKeys = (e)=>{
window.onbeforeunload = ()=>{ if(!(e.ctrlKey || e.metaKey)) return;
if(isSaving || unsavedChanges) if(e.keyCode === 83) trySaveRef.current(true);
return 'You have unsaved changes!'; 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 ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
window.onbeforeunload = null; window.onBeforeUnload = null;
}; };
}, []); }, []);
useEffect(()=>{ useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, savedBrew.current); trySaveRef.current = trySave;
unsavedChangesRef.current = unsavedChanges;
});
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange); setUnsavedChanges(hasChange);
if(hasChange && autoSaveEnabled) trySave(); if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]); }, [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 = ()=>{ const handleSplitMove = ()=>{
editorRef.current?.update(); editorRef.current?.update();
}; };
@@ -123,29 +132,27 @@ const EditPage = (props)=>{
setCurrentBrewRendererPageNum(pageNumber); setCurrentBrewRendererPageNum(pageNumber);
}; };
const handleTextChange = (text)=>{ 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 there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(text)); setHTMLErrors(Markdown.validate(value));
setCurrentBrew((prevBrew)=>({ ...prevBrew, text }));
};
const handleStyleChange = (style)=>{ if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
setCurrentBrew((prevBrew)=>({ ...prevBrew, style })); else setCurrentBrew(prev => ({ ...prev, [field]: value }));
};
const handleSnipChange = (snippet)=>{ if(useLocalStorage) {
//If there are HTML errors, run the validator on every change to give quick feedback if(field == 'text') localStorage.setItem(BREWKEY, value);
if(HTMLErrors.length) if(field == 'style') localStorage.setItem(STYLEKEY, value);
setHTMLErrors(Markdown.validate(snippet)); if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet })); if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
}; renderer : value.renderer,
theme : value.theme,
const handleMetaChange = (metadata, field = undefined)=>{ lang : value.lang
if(field === 'theme' || field === 'renderer') }));
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); }
setCurrentBrew((prev)=>({ ...prev, ...metadata }));
}; };
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
@@ -155,22 +162,10 @@ const EditPage = (props)=>{
snippets : newData.snippets snippets : newData.snippets
})); }));
const trySave = (immediate = false)=>{ const resetWarnUnsavedTimer = ()=>{
//debounceSave = _.debounce(save, SAVE_TIMEOUT); setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
if(isSaving) return; clearTimeout(warnUnsavedTimeout.current);
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
const hasChange = !_.isEqual(currentBrew, savedBrew.current);
if(immediate) {
debounceSave(currentBrew, saveGoogle);
debounceSave.flush?.();
return;
}
if(hasChange)
debounceSave(currentBrew, saveGoogle);
else
debounceSave.cancel?.();
}; };
const handleGoogleClick = ()=>{ const handleGoogleClick = ()=>{
@@ -196,11 +191,26 @@ const EditPage = (props)=>{
trySave(true); trySave(true);
}; };
const save = async (brew, saveToGoogle)=>{ const trySave = (immediate = false, hasChanges = true)=>{
debounceSave?.cancel?.(); clearTimeout(saveTimeout.current);
if(isSaving) return;
if(!hasChanges && !immediate) return;
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
setIsSaving(true); saveTimeout.current = setTimeout(async ()=>{
setError(null); 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)); setHTMLErrors(Markdown.validate(brew.text));
await updateHistory(brew).catch(console.error); await updateHistory(brew).catch(console.error);
@@ -211,9 +221,10 @@ const EditPage = (props)=>{
...brew, ...brew,
text : brew.text.normalize('NFC'), text : brew.text.normalize('NFC'),
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, 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')))), patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
hash : await md5(savedBrew.current.text), hash : await md5(lastSavedBrew.current.text),
textBin : undefined textBin : undefined,
version : lastSavedBrew.current.version
}; };
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave)));
@@ -231,22 +242,24 @@ const EditPage = (props)=>{
}); });
if(!res) return; if(!res) return;
const { googleId, editId, shareId, version } = res.body; const updatedFields = {
googleId : res.body.googleId ?? null,
savedBrew.current = { editId : res.body.editId,
...brew, shareId : res.body.shareId,
googleId : googleId ?? null, version : res.body.version
editId,
shareId,
version
}; };
setCurrentBrew(savedBrew.current); lastSavedBrew.current = {
...brew,
...updatedFields
};
setIsSaving(false); setCurrentBrew((prevBrew)=>({
setUnsavedTime(new Date()); ...prevBrew,
...updatedFields
}));
history.replaceState(null, null, `/edit/${editId}`); history.replaceState(null, null, `/edit/${res.body.editId}`);
}; };
const renderGoogleDriveIcon = ()=>( const renderGoogleDriveIcon = ()=>(
@@ -267,7 +280,7 @@ const EditPage = (props)=>{
{alertLoginToTransfer && ( {alertLoginToTransfer && (
<div className='errorContainer' onClick={closeAlerts}> <div className='errorContainer' onClick={closeAlerts}>
You must be signed in to a Google account to transfer between the homebrewery and Google Drive! You must be signed in to a Google account to transfer between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${url}`}> <a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'> Sign In </div> <div className='confirm'> Sign In </div>
</a> </a>
<div className='deny'> Not Now </div> <div className='deny'> Not Now </div>
@@ -290,17 +303,17 @@ const EditPage = (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 && autoSaveWarning) { if(unsavedChanges && warnUnsavedChanges) {
resetAutoSaveWarning(); resetWarnUnsavedTimer();
const elapsedTime = Math.round((new Date() - unsavedTime) / 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.'
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; : `Autosave is OFF, 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
@@ -316,15 +329,11 @@ const EditPage = (props)=>{
}; };
const toggleAutoSave = ()=>{ const toggleAutoSave = ()=>{
if(warningTimer.current) clearTimeout(warningTimer.current); clearTimeout(warnUnsavedTimeout.current);
clearTimeout(saveTimeout.current);
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled));
setAutoSaveWarning(autoSaveWarning);
setAutoSaveEnabled(!autoSaveEnabled); setAutoSaveEnabled(!autoSaveEnabled);
}; setWarnUnsavedChanges(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
}; };
const renderAutoSaveButton = ()=>( const renderAutoSaveButton = ()=>(
@@ -333,31 +342,12 @@ const EditPage = (props)=>{
</Nav.item> </Nav.item>
); );
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 = ()=>{ const clearError = ()=>{
setError(null); setError(null);
setIsSaving(false); setIsSaving(false);
}; };
const renderNavbar = ()=>{ const renderNavbar = ()=>{
const shareLink = processShareId();
return <Navbar> return <Navbar>
<Nav.section> <Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item> <Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
@@ -368,25 +358,12 @@ const EditPage = (props)=>{
{error {error
? <ErrorNavItem error={error} clearError={clearError} /> ? <ErrorNavItem error={error} clearError={clearError} />
: <Nav.dropdown className='save-menu'> : <Nav.dropdown className='save-menu'>
{renderSaveButton()} {renderSaveButton()}
{renderAutoSaveButton()} {renderAutoSaveButton()}
</Nav.dropdown>} </Nav.dropdown>}
<NewBrewItem/> <NewBrewItem/>
<HelpNavItem/> <HelpNavItem/>
<Nav.dropdown> <ShareNavItem brew={currentBrew} />
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item>
<Nav.item color='blue' href={`/share/${shareLink}`}>
view
</Nav.item>
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
copy url
</Nav.item>
<Nav.item color='blue' href={getRedditLink()} newTab={true} rel='noopener noreferrer'>
post to reddit
</Nav.item>
</Nav.dropdown>
<PrintNavItem /> <PrintNavItem />
<VaultNavItem /> <VaultNavItem />
<RecentNavItem brew={currentBrew} storageKey='edit' /> <RecentNavItem brew={currentBrew} storageKey='edit' />
@@ -408,10 +385,7 @@ const EditPage = (props)=>{
<Editor <Editor
ref={editorRef} ref={editorRef}
brew={currentBrew} brew={currentBrew}
onTextChange={handleTextChange} onBrewChange={handleBrewChange}
onStyleChange={handleStyleChange}
onSnipChange={handleSnipChange}
onMetaChange={handleMetaChange}
reportError={setError} reportError={setError}
renderer={currentBrew.renderer} renderer={currentBrew.renderer}
userThemes={props.userThemes} userThemes={props.userThemes}

View File

@@ -3,6 +3,7 @@ import './homePage.less';
import React from 'react'; import React from 'react';
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import { Meta } from 'vitreum/headtags'; import { Meta } from 'vitreum/headtags';
import Nav from 'naturalcrit/nav/nav.jsx'; import Nav from 'naturalcrit/nav/nav.jsx';
@@ -21,6 +22,11 @@ import BrewRenderer from '../../brewRenderer/brewRenderer.jsx
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const HomePage =(props)=>{ const HomePage =(props)=>{
props = { props = {
brew : DEFAULT_BREW, brew : DEFAULT_BREW,
@@ -28,9 +34,10 @@ const HomePage =(props)=>{
...props ...props
}; };
const [brew , setBrew] = useState(props.brew); const [currentBrew , setCurrentBrew] = useState(props.brew);
const [welcomeText , setWelcomeText] = useState(props.brew.text); const [welcomeText , setWelcomeText] = useState(props.brew.text);
const [error , setError] = useState(undefined); const [error , setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
@@ -39,13 +46,15 @@ const HomePage =(props)=>{
const editorRef = useRef(null); const editorRef = useRef(null);
const useLocalStorage = false;
useEffect(()=>{ useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
}, []); }, []);
const save = ()=>{ const save = ()=>{
request.post('/api') request.post('/api')
.send(brew) .send(currentBrew)
.end((err, res)=>{ .end((err, res)=>{
if(err) { if(err) {
setError(err); setError(err);
@@ -72,8 +81,27 @@ const HomePage =(props)=>{
setCurrentBrewRendererPageNum(pageNumber); setCurrentBrewRendererPageNum(pageNumber);
}; };
const handleTextChange = (text)=>{ const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
setBrew((prevBrew) => ({ ...prevBrew, text })); 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(Markdown.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 clearError = ()=>{ const clearError = ()=>{
@@ -105,9 +133,9 @@ const HomePage =(props)=>{
<SplitPane onDragFinish={handleSplitMove}> <SplitPane onDragFinish={handleSplitMove}>
<Editor <Editor
ref={editorRef} ref={editorRef}
brew={brew} brew={currentBrew}
onTextChange={handleTextChange} onBrewChange={handleBrewChange}
renderer={brew.renderer} renderer={currentBrew.renderer}
showEditButtons={false} showEditButtons={false}
themeBundle={themeBundle} themeBundle={themeBundle}
onCursorPageChange={handleEditorCursorPageChange} onCursorPageChange={handleEditorCursorPageChange}
@@ -117,9 +145,9 @@ const HomePage =(props)=>{
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={brew.text} text={currentBrew.text}
style={brew.style} style={currentBrew.style}
renderer={brew.renderer} renderer={currentBrew.renderer}
onPageChange={handleBrewRendererPageChange} onPageChange={handleBrewRendererPageChange}
currentEditorViewPageNum={currentEditorViewPageNum} currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum} currentEditorCursorPageNum={currentEditorCursorPageNum}
@@ -128,7 +156,7 @@ const HomePage =(props)=>{
/> />
</SplitPane> </SplitPane>
</div> </div>
<div className={`floatingSaveButton${welcomeText !== brew.text ? ' show' : ''}`} onClick={save}> <div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}>
Save current <i className='fas fa-save' /> Save current <i className='fas fa-save' />
</div> </div>

View File

@@ -22,8 +22,9 @@ import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '.
const BREWKEY = 'homebrewery-new'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta'; const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; const SAVEKEYPREFIX = 'HOMEBREWERY-DEFAULT-SAVE-LOCATION-';
const NewPage = (props) => { const NewPage = (props) => {
props = { props = {
@@ -43,6 +44,8 @@ const NewPage = (props) => {
const editorRef = useRef(null); const editorRef = useRef(null);
const useLocalStorage = true;
useEffect(() => { useEffect(() => {
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
loadBrew(); loadBrew();
@@ -67,6 +70,7 @@ const NewPage = (props) => {
brew.lang = metaStorage?.lang ?? brew.lang; brew.lang = metaStorage?.lang ?? brew.lang;
} }
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew); setCurrentBrew(brew);
@@ -108,40 +112,27 @@ const NewPage = (props) => {
setCurrentBrewRendererPageNum(pageNumber); setCurrentBrewRendererPageNum(pageNumber);
}; };
const handleTextChange = (text)=>{ 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 there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
HTMLErrors = Markdown.validate(text); setHTMLErrors(Markdown.validate(value));
setHTMLErrors(HTMLErrors); if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); else setCurrentBrew(prev => ({ ...prev, [field]: value }));
localStorage.setItem(BREWKEY, text);
};
const handleStyleChange = (style) => { if(useLocalStorage) {
setCurrentBrew(prevBrew => ({ ...prevBrew, style })); if(field == 'text') localStorage.setItem(BREWKEY, value);
localStorage.setItem(STYLEKEY, style); if(field == 'style') localStorage.setItem(STYLEKEY, value);
}; if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
const handleSnipChange = (snippet)=>{ renderer : value.renderer,
//If there are HTML errors, run the validator on every change to give quick feedback theme : value.theme,
if(HTMLErrors.length) lang : value.lang
HTMLErrors = Markdown.validate(snippet); }));
}
setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet }));
};
const handleMetaChange = (metadata, field = undefined) => {
if (field === 'theme' || field === 'renderer')
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setCurrentBrew(prev => ({ ...prev, ...metadata }));
localStorage.setItem(METAKEY, JSON.stringify({
renderer : metadata.renderer,
theme : metadata.theme,
lang : metadata.lang
}));
}; };
const save = async () => { const save = async () => {
@@ -215,10 +206,7 @@ const NewPage = (props) => {
<Editor <Editor
ref={editorRef} ref={editorRef}
brew={currentBrew} brew={currentBrew}
onTextChange={handleTextChange} onBrewChange={handleBrewChange}
onStyleChange={handleStyleChange}
onMetaChange={handleMetaChange}
onSnipChange={handleSnipChange}
renderer={currentBrew.renderer} renderer={currentBrew.renderer}
userThemes={props.userThemes} userThemes={props.userThemes}
themeBundle={themeBundle} themeBundle={themeBundle}

View File

@@ -38,15 +38,6 @@
animation-duration : 0.4s; animation-duration : 0.4s;
} }
.CodeMirror-vscrollbar {
&::-webkit-scrollbar { width : 20px; }
&::-webkit-scrollbar-thumb {
width : 20px;
background : linear-gradient(90deg, #858585 15px, #808080 15px);
}
}
//.cm-tab { //.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
//} //}