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

Compare commits

..

41 Commits

Author SHA1 Message Date
Trevor Buckner
63bebe1efd Lint everything
Catching up on a bunch of linting so random changes stop showing up on PRs when the linter is run.
2025-10-06 00:02:24 -04:00
Trevor Buckner
22e26d635a Merge pull request #4460 from naturalcrit/cleanupLocalStorageKeysTest
Clean up localStorageMap code
2025-10-05 23:28:34 -04:00
Trevor Buckner
643e0ac650 small cleanups of localstorage keys code 2025-10-05 23:24:50 -04:00
Trevor Buckner
5395412ac5 Remove tests for getLocalStorageMap()
The function is a simple getter with trivial logic; test is effectively just asserting the size of the map, which coverage adds no meaningful value and adds cruft to the codebase.
2025-10-05 23:24:35 -04:00
Trevor Buckner
dc4610ea1b Merge pull request #4447 from dbolack-ab/issue_3426
Applies G-Ambatte's fix for Firefox browser lock.
2025-10-05 22:31:15 -04:00
Trevor Buckner
1e71e9e18a Use blockquote and table elements, not .classes 2025-10-05 22:19:43 -04:00
Trevor Buckner
4203e90d09 Merge branch 'master' into issue_3426 2025-10-05 22:09:06 -04:00
Trevor Buckner
dc94555c94 Merge pull request #4458 from naturalcrit/new/edit/home_commonSaveButton
Make the renderSaveButton() function common between edit/new/home
2025-10-05 22:03:32 -04:00
Trevor Buckner
41aebf084b Make the renderSaveButton() function common between edit/new/home
Each of the edit/home/new pages renders its save button differently. This makes it a common function with all the same possible render states (does the document have unsaved changes? Is it already saved? Was it auto-saved?).

- Common save button
- Adds the "save" button to /home page which wasn't there before
- Animates the "save" button in /home and /new when the user makes their first change to signal that yes, you do have to actually click the save button if you want to keep this.
- "reminder... you haven't saved for X minutes" still not functional on /new and /home since that involves more moving pieces.
2025-10-05 21:57:19 -04:00
David Bolack
74e17e154f Merge branch 'issue_3426' of github.com:dbolack-ab/homebrewery into issue_3426 2025-10-05 20:12:32 -05:00
Trevor Buckner
a944b23ca0 Merge pull request #4457 from naturalcrit/new/home/edit/_unsavedChanges_common
Make `unsavedChanges` state common
2025-10-05 20:09:14 -04:00
Trevor Buckner
12052853db Merge branch 'master' into new/home/edit/_unsavedChanges_common 2025-10-05 20:07:56 -04:00
Trevor Buckner
c0f67bef5a Merge pull request #4434 from naturalcrit/fix-red-background
Fix dev background
2025-10-05 19:51:01 -04:00
David Bolack
8f715a6615 Isolate change to Firefox 2025-10-05 18:36:14 -05:00
Víctor Losada Hernández
1f51abaf10 this makes more sense 2025-10-05 19:57:49 +02:00
Víctor Losada Hernández
c90a8c53a5 lets test this 2025-10-05 19:56:50 +02:00
Víctor Losada Hernández
ac18f4bd1d Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-05 19:43:29 +02:00
Víctor Losada Hernández
7393aef806 set up development config variavle 2025-10-05 19:42:01 +02:00
Trevor Buckner
2c4c4b8f92 Make unsavedChanges state common
/editPage.jsx uses `unsavedChanges` state to detect when autosave should fire, or unsaved changes warning should display.

/homePage.jsx uses a similar check (different variables) to detect when to show the popup "save now"! button

/newPage.jsx doesn't do any of this, but probably should pop up a warning when saving hasn't happened for a long time

This commit just gives all of the pages the same common `unsavedChanges` state, calculated in the same way, and updates any sections that depend on that updated state.

This is precursor work to adding "unsaved changes" warnings to all three pages.
2025-10-04 22:17:24 -04:00
Trevor Buckner
c751d647d9 Merge pull request #4440 from naturalcrit/UnifyNewHomeEdit-Structure&Naming
Clean Up Common features of new/home/edit
2025-10-04 21:52:28 -04:00
Trevor Buckner
8f7ae35f08 Merge branch 'master' into issue_3426 2025-10-04 18:32:15 -04:00
David Bolack
f0bb06e706 Merge branch 'master' into issue_3426 2025-10-03 18:52:56 -05:00
Víctor Losada Hernández
aff9a85769 end of file character shit 2025-10-03 21:38:43 +02:00
Víctor Losada Hernández
e0379a0baa last cleanup 2025-10-03 21:38:10 +02:00
Víctor Losada Hernández
e8a0681015 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-03 21:37:06 +02:00
Víctor Losada Hernández
3ed61ebe2c Merge branch 'fix-red-background' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-03 21:32:55 +02:00
Víctor Losada Hernández
c2e51b0baa removing isclient check to see what's what 2025-10-03 21:32:52 +02:00
Trevor Buckner
51b91567f6 Merge branch 'master' into fix-red-background 2025-10-02 18:39:31 -04:00
Víctor Losada Hernández
eefda9fe45 simplifying per suggestion 2025-10-02 12:40:12 +02:00
Víctor Losada Hernández
e793db7b37 separating the words to make it less ugly 2025-10-01 22:55:32 +02:00
Víctor Losada Hernández
ff5450ad8c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-09-29 22:28:12 +02:00
David Bolack
c50c279ef3 Merge branch 'issue_3426' of github.com:dbolack-ab/homebrewery into issue_3426 2025-09-22 20:36:45 -05:00
David Bolack
cc246fb31a Merge branch 'master' into issue_3426 2025-09-22 20:36:09 -05:00
Víctor Losada Hernández
4c5eef46a0 Merge branch 'master' into issue_3426 2025-08-21 16:33:19 +02:00
David Bolack
a1ab27b57f Applies G-Ambatte's fix 2025-07-30 19:47:59 -05:00
Víctor Losada Hernández
d6a5a1f03c no idea what changed but now it works 2025-07-18 00:39:36 +02:00
Víctor Losada Hernández
f04d6cdd1f fix to current 2025-07-17 23:32:18 +02:00
Víctor Losada Hernández
4fd61ce92c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-07-17 23:30:01 +02:00
Víctor Losada Hernández
88b70d340e final bit 2025-05-27 11:27:04 +02:00
Víctor Losada Hernández
ed05d8c754 move all to homebrew.jsx 2025-05-27 11:25:01 +02:00
Víctor Losada Hernández
077aaeb815 log 2025-05-27 10:54:07 +02:00
23 changed files with 215 additions and 135 deletions

View File

@@ -295,12 +295,6 @@ const BrewRenderer = (props)=>{
rowGap : `${displayOptions.rowGap}px` rowGap : `${displayOptions.rowGap}px`
}; };
const styleObject = {};
if(global.config.deployment) {
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
}
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]); const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]); renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
@@ -329,10 +323,9 @@ const BrewRenderer = (props)=>{
contentDidMount={frameDidMount} contentDidMount={frameDidMount}
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`} <div className='brewRenderer'
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={ styleObject }
> >
{/* Apply CSS from Style tab and render pages from Markdown tab */} {/* Apply CSS from Style tab and render pages from Markdown tab */}

View File

@@ -6,7 +6,6 @@
overflow-y : scroll; overflow-y : scroll;
will-change : transform; will-change : transform;
&:has(.facing, .flow) { padding : 60px 30px; } &:has(.facing, .flow) { padding : 60px 30px; }
&.deployment { background-color : darkred; }
:where(.pages) { :where(.pages) {
&.facing { &.facing {
display : grid; display : grid;

View File

@@ -18,7 +18,7 @@ module.exports = {
try { try {
Boolean(new URL(value)); Boolean(new URL(value));
return null; return null;
} catch (e) { } catch {
return 'Must be a valid URL'; return 'Must be a valid URL';
} }
} }

View File

@@ -19,7 +19,6 @@ const WithRoute = ({ el: Element, ...rest })=>{
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const queryParams = Object.fromEntries(searchParams?.entries() || []); const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />; return <Element {...rest} {...params} query={queryParams} />;
}; };
@@ -50,11 +49,20 @@ const Homebrew = (props)=>{
global.enable_themes = enable_themes; global.enable_themes = enable_themes;
global.config = config; global.config = config;
const backgroundObject = ()=>{
if(global.config.deployment || (config.local && config.development)){
const bgText = global.config.deployment || 'Local';
return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
};
}
return null;
};
updateLocalStorage(); updateLocalStorage();
return ( return (
<Router location={url}> <Router location={url}>
<div className='homebrew'> <div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes> <Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} /> <Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} /> <Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />

View File

@@ -1,12 +1,14 @@
@import 'naturalcrit/styles/core.less'; @import 'naturalcrit/styles/core.less';
.homebrew { .homebrew {
height : 100%; height : 100%;
background-color:@steel;
&.deployment { background-color : darkred; }
.sitePage { .sitePage {
display : flex; display : flex;
flex-direction : column; flex-direction : column;
height : 100%; height : 100%;
overflow-y : hidden; overflow-y : hidden;
background-color : @steel;
.content { .content {
position : relative; position : relative;
flex : auto; flex : auto;

View File

@@ -4,7 +4,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const ErrorNavItem = ({ error = '', clearError })=>{ const ErrorNavItem = ({ error = '', clearError })=>{
const response = error.response; const response = error.response;
const errorCode = error.code const errorCode = error.code;
const status = response?.status; const status = response?.status;
const HBErrorCode = response?.body?.HBErrorCode; const HBErrorCode = response?.body?.HBErrorCode;
const message = response?.body?.message; const message = response?.body?.message;
@@ -15,7 +15,7 @@ const ErrorNavItem = ({error = '', clearError})=>{
errMsg += `\`\`\`\n${error.stack}\n`; errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
console.log(errMsg); console.log(errMsg);
} catch (e){} } catch {}
if(status === 409) { if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>

View File

@@ -35,7 +35,7 @@ const NewBrew = ()=>{
const type = file.name.split('.').pop().toLowerCase(); const type = file.name.split('.').pop().toLowerCase();
alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`); alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file); console.log(file);

View File

@@ -5,6 +5,7 @@ import './editPage.less';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js'; import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
@@ -25,7 +26,6 @@ import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports // Page specific imports
import { Meta } from 'vitreum/headtags'; import { Meta } from 'vitreum/headtags';
import _ from 'lodash';
import { md5 } from 'hash-wasm'; import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate'; import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch'; import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
@@ -46,8 +46,8 @@ const STYLEKEY = 'HB_newPage_style';
const SNIPKEY = 'HB_newPage_snippets'; 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 EditPage = (props)=>{ const EditPage = (props)=>{
props = { props = {
@@ -131,8 +131,8 @@ const EditPage = (props)=>{
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value)); setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value })); else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) { if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value); if(field == 'text') localStorage.setItem(BREWKEY, value);
@@ -309,14 +309,18 @@ const EditPage = (props)=>{
// #3 - Unsaved changes exist, click to save, show SAVE NOW // #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges) if(unsavedChanges)
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>; return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
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
if(neverSaved)
return <Nav.item className='save neverSaved'>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>;
}; };
const toggleAutoSave = ()=>{ const toggleAutoSave = ()=>{

View File

@@ -5,6 +5,7 @@ import './homePage.less';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js'; import Markdown from 'naturalcrit/markdown.js';
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';
@@ -32,6 +33,7 @@ const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta'; const METAKEY = 'homebrewery-new-meta';
const useLocalStorage = false; const useLocalStorage = false;
const neverSaved = true;
const HomePage =(props)=>{ const HomePage =(props)=>{
props = { props = {
@@ -41,16 +43,18 @@ const HomePage =(props)=>{
}; };
const [currentBrew , setCurrentBrew] = useState(props.brew); const [currentBrew , setCurrentBrew] = useState(props.brew);
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 [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);
const [themeBundle , setThemeBundle] = useState({}); const [themeBundle , setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = useState(false);
const [isSaving , setIsSaving] = useState(false); const [isSaving , setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{ useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
@@ -85,6 +89,13 @@ const HomePage =(props)=>{
}); });
}; };
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{ const handleSplitMove = ()=>{
editorRef.current.update(); editorRef.current.update();
}; };
@@ -97,8 +108,8 @@ const HomePage =(props)=>{
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value)); setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value })); else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) { if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value); if(field == 'text') localStorage.setItem(BREWKEY, value);
@@ -112,6 +123,41 @@ const HomePage =(props)=>{
} }
}; };
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(isSaving)
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
// 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 <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{ const clearError = ()=>{
setError(null); setError(null);
setIsSaving(false); setIsSaving(false);
@@ -120,10 +166,9 @@ const HomePage =(props)=>{
const renderNavbar = ()=>{ const renderNavbar = ()=>{
return <Navbar ver={props.ver}> return <Navbar ver={props.ver}>
<Nav.section> <Nav.section>
{error ? {error
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> : ? <ErrorNavItem error={error} clearError={clearError} />
null : renderSaveButton()}
}
<NewBrewItem /> <NewBrewItem />
<PrintNavItem /> <PrintNavItem />
<HelpNavItem /> <HelpNavItem />
@@ -165,7 +210,7 @@ const HomePage =(props)=>{
/> />
</SplitPane> </SplitPane>
</div> </div>
<div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}> <div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
Save current <i className='fas fa-save' /> Save current <i className='fas fa-save' />
</div> </div>
@@ -173,7 +218,7 @@ const HomePage =(props)=>{
Create your own <i className='fas fa-magic' /> Create your own <i className='fas fa-magic' />
</a> </a>
</div> </div>
) );
}; };
module.exports = HomePage; module.exports = HomePage;

View File

@@ -34,7 +34,13 @@
} }
.navItem.save { .navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange; background-color : @orange;
&:hover { background-color : @green; } &:hover { background-color : @green; }
&.neverSaved {
.fadeOutRight();
opacity: 0;
}
} }
} }

View File

@@ -5,6 +5,7 @@ import './newPage.less';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js'; import Markdown from 'naturalcrit/markdown.js';
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';
@@ -26,15 +27,14 @@ import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports // Page specific imports
import { Meta } from 'vitreum/headtags'; import { Meta } from 'vitreum/headtags';
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 METAKEY = 'HB_newPage_metadata';
const SNIPKEY = 'HB_newPage_snippets'; const SNIPKEY = 'HB_newPage_snippets';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_'; const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true; const useLocalStorage = true;
const neverSaved = true;
const NewPage = (props)=>{ const NewPage = (props)=>{
props = { props = {
@@ -51,8 +51,11 @@ const NewPage = (props) => {
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({}); const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{ useEffect(()=>{
loadBrew(); loadBrew();
@@ -93,6 +96,7 @@ const NewPage = (props) => {
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew); setCurrentBrew(brew);
lastSavedBrew.current = brew;
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle); setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
localStorage.setItem(BREWKEY, brew.text); localStorage.setItem(BREWKEY, brew.text);
@@ -103,6 +107,13 @@ const NewPage = (props) => {
window.history.replaceState({}, window.location.title, '/new/'); window.history.replaceState({}, window.location.title, '/new/');
}; };
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{ const handleSplitMove = ()=>{
editorRef.current.update(); editorRef.current.update();
}; };
@@ -115,8 +126,8 @@ const NewPage = (props) => {
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value)); setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value })); else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) { if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value); if(field == 'text') localStorage.setItem(BREWKEY, value);
@@ -133,7 +144,7 @@ const NewPage = (props) => {
const save = async ()=>{ const save = async ()=>{
setIsSaving(true); setIsSaving(true);
let updatedBrew = { ...currentBrew }; const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew); splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm; const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
@@ -147,7 +158,7 @@ const NewPage = (props) => {
setError(err); setError(err);
}); });
setIsSaving(false) setIsSaving(false);
if(!res) return; if(!res) return;
const savedBrew = res.body; const savedBrew = res.body;
@@ -159,15 +170,38 @@ const NewPage = (props) => {
}; };
const renderSaveButton = ()=>{ const renderSaveButton = ()=>{
if(isSaving){ // #1 - Currently saving, show SAVING
return <Nav.item icon='fas fa-spinner fa-spin' className='save'> if(isSaving)
save... return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
</Nav.item>;
} else { // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
return <Nav.item icon='fas fa-save' className='save' onClick={save}> // if(unsavedChanges && warnUnsavedChanges) {
save // resetWarnUnsavedTimer();
</Nav.item>; // 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 <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
}; };
const clearError = ()=>{ const clearError = ()=>{

View File

@@ -1,6 +1,12 @@
.newPage { .newPage {
.navItem.save { .navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange; background-color : @orange;
&:hover { background-color : @green; } &:hover { background-color : @green; }
&.neverSaved {
.fadeOutRight();
opacity: 0;
}
} }
} }

View File

@@ -1,14 +1,16 @@
.vaultPage { .vaultPage {
height : 100%; height : 100%;
overflow-y : hidden; overflow-y : hidden;
background-color : #2C3E50;
*:not(input) { user-select : none; } *:not(input) { user-select : none; }
.form {
background:white;
}
:where(.content .dataGroup) { :where(.content .dataGroup) {
width : 100%; width : 100%;
height : 100%; height : 100%;
background : white;
&.form .brewLookup { &.form .brewLookup {
position : relative; position : relative;
@@ -171,7 +173,6 @@
max-height : 100%; max-height : 100%;
padding : 70px 50px; padding : 70px 50px;
overflow-y : scroll; overflow-y : scroll;
background-color : #2C3E50;
container-type : inline-size; container-type : inline-size;
h3 { font-size : 25px; } h3 { font-size : 25px; }

View File

@@ -1,30 +0,0 @@
import getLocalStorageMap from './localStorageKeyMap.js';
describe('getLocalStorageMap', ()=>{
it('no username', ()=>{
const account = global.account;
delete global.account;
const map = getLocalStorageMap();
global.account = account;
expect(map).toBeInstanceOf(Object);
expect(Object.entries(map)).toHaveLength(16);
});
it('no username', ()=>{
const account = global.account;
global.account = { username: 'test' };
const map = getLocalStorageMap();
global.account = account;
expect(map).toBeInstanceOf(Object);
expect(Object.entries(map)).toHaveLength(17);
expect(map).toHaveProperty('HOMEBREWERY-DEFAULT-SAVE-LOCATION-test', 'HB_editor_defaultSave_test');
});
});

View File

@@ -4,10 +4,7 @@ const updateLocalStorage = function(){
// Return if no window and thus no local storage // Return if no window and thus no local storage
if(typeof window === 'undefined') return; if(typeof window === 'undefined') return;
// Return if the local storage key map has no content
const localStorageKeyMap = getLocalStorageMap(); const localStorageKeyMap = getLocalStorageMap();
if(Object.keys(localStorageKeyMap).length == 0) return;
const storage = window.localStorage; const storage = window.localStorage;
Object.keys(localStorageKeyMap).forEach((key)=>{ Object.keys(localStorageKeyMap).forEach((key)=>{

View File

@@ -1,4 +1,5 @@
{ {
"development": true,
"host" : "homebrewery.local.naturalcrit.com:8000", "host" : "homebrewery.local.naturalcrit.com:8000",
"naturalcrit_url" : "local.naturalcrit.com:8010", "naturalcrit_url" : "local.naturalcrit.com:8010",
"secret" : "secret", "secret" : "secret",

View File

@@ -377,7 +377,7 @@ const api = {
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]); const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
if(patchedResult != brewFromClient.text) if(patchedResult != brewFromClient.text)
throw("Patches did not apply cleanly, text mismatch detected"); throw ('Patches did not apply cleanly, text mismatch detected');
// brew.text = applyPatches(patches, brewFromServer.text)[0]; // brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) { } catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);

View File

@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
const mpAsSnippets = []; const mpAsSnippets = [];
// Snippets from Themes first. // Snippets from Themes first.
if(themeBundleSnippets) { if(themeBundleSnippets) {
for (let themes of themeBundleSnippets) { for (const themes of themeBundleSnippets) {
if(typeof themes !== 'string') { if(typeof themes !== 'string') {
const userSnippets = []; const userSnippets = [];
const snipSplit = themes.snippets.trim().split(textSplit).slice(1); const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
@@ -77,8 +77,8 @@ const yamlSnippetsToText = (yamlObj)=>{
let snippetsText = ''; let snippetsText = '';
for (let snippet of yamlObj) { for (const snippet of yamlObj) {
for (let subSnippet of snippet.subsnippets) { for (const subSnippet of snippet.subsnippets) {
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`; snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
} }
} }
@@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
const res = await request const res = await request
.get(`/api/theme/${renderer}/${theme}`) .get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{ .catch((err)=>{
setError(err) setError(err);
}); });
if(!res) { if(!res) {
setThemeBundle({}); setThemeBundle({});
@@ -166,7 +166,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
break; break;
} }
} }
} };
export { export {
splitTextStyleAndMetadata, splitTextStyleAndMetadata,

View File

@@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
try { try {
return mathParser.evaluate(replacedLabel); return mathParser.evaluate(replacedLabel);
} catch (error) { } catch {
return undefined; // Return undefined if invalid math result return undefined; // Return undefined if invalid math result
} }
} }

View File

@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
prot = decodeURIComponent(unescape(href)) prot = decodeURIComponent(unescape(href))
.replace(nonWordAndColonTest, '') .replace(nonWordAndColonTest, '')
.toLowerCase(); .toLowerCase();
} catch (e) { } catch {
return null; return null;
} }
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
} }
try { try {
href = encodeURI(href).replace(/%25/g, '%'); href = encodeURI(href).replace(/%25/g, '%');
} catch (e) { } catch {
return null; return null;
} }
return href; return href;

View File

@@ -611,3 +611,17 @@ h6,
} }
.toc.wide li { break-inside : auto; } .toc.wide li { break-inside : auto; }
} }
/**********************************
Firefox endruns
**********************************/
@supports (-moz-user-select: none) { // This section will only apply to Firefox; it's the only browser that supports `-mos-xyz...`
.page {
blockquote, table {
page-break-inside: auto;
break-inside: auto;
}
}
}