0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-25 16:15:53 +00:00

Compare commits

..

44 Commits

Author SHA1 Message Date
Trevor Buckner
7fc03bb5a7 Merge branch 'master' into PreventInterruptedBrewJump 2025-10-06 00:07:43 -04:00
Trevor Buckner
ccf11661de Merge pull request #4461 from naturalcrit/LintEverythingOct2025
Lint everything
2025-10-06 00:07:29 -04:00
Trevor Buckner
811f274968 Move badly-placed scrollingTimeout that was doing nothing
When a user clicks brewJump or sourceJump, we disallow new jumps until the current scroll operation has finished for 150 ms. Unfortunately the timer being checked was always undefined, so you could keep clicking the jump button and get the brewRenderer or editor to keep bouncing around without finishing the jump action.

This just moves the scrollingTimeout up outside of the listener function so it isn't being reset to undefined every loop.
2025-10-06 00:06:34 -04:00
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
24 changed files with 221 additions and 141 deletions

View File

@@ -295,12 +295,6 @@ const BrewRenderer = (props)=>{
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]);
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
@@ -329,10 +323,9 @@ const BrewRenderer = (props)=>{
contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
>
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
<div className='brewRenderer'
onKeyDown={handleControlKeys}
tabIndex={-1}
style={ styleObject }
>
{/* Apply CSS from Style tab and render pages from Markdown tab */}

View File

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

View File

@@ -325,10 +325,10 @@ const Editor = createClass({
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
const checkIfScrollComplete = ()=>{
let scrollingTimeout;
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
@@ -369,8 +369,8 @@ const Editor = createClass({
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
const checkIfScrollComplete = ()=>{
let scrollingTimeout;
let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;

View File

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

View File

@@ -19,7 +19,6 @@ const WithRoute = ({ el: Element, ...rest })=>{
const params = useParams();
const [searchParams] = useSearchParams();
const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />;
};
@@ -50,11 +49,20 @@ const Homebrew = (props)=>{
global.enable_themes = enable_themes;
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();
return (
<Router location={url}>
<div className='homebrew'>
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />

View File

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

View File

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

View File

@@ -34,8 +34,8 @@ const NewBrew = ()=>{
}
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);

View File

@@ -2,22 +2,22 @@ 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 getShareId = (brew)=>(
brew.googleId && !brew.stubbed
? brew.googleId + brew.shareId
: brew.shareId
);
const getRedditLink = (brew)=>{
const text = dedent`
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)}`;
};
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
export default ({brew}) => (
export default ({ brew })=>(
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share

View File

@@ -5,6 +5,7 @@ import './editPage.less';
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.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
import { Meta } from 'vitreum/headtags';
import _ from 'lodash';
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
@@ -46,8 +46,8 @@ const STYLEKEY = 'HB_newPage_style';
const SNIPKEY = 'HB_newPage_snippets';
const METAKEY = 'HB_newPage_meta';
const useLocalStorage = false;
const neverSaved = false;
const EditPage = (props)=>{
props = {
@@ -123,16 +123,16 @@ const EditPage = (props)=>{
editorRef.current?.update();
};
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme')
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(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
@@ -309,14 +309,18 @@ const EditPage = (props)=>{
// #3 - Unsaved changes exist, click to save, show SAVE NOW
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
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
return <Nav.item className='save saved'>saved.</Nav.item>;
return <Nav.item className='save saved'>saved</Nav.item>;
};
const toggleAutoSave = ()=>{

View File

@@ -5,6 +5,7 @@ import './homePage.less';
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
@@ -32,25 +33,28 @@ const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const useLocalStorage = false;
const neverSaved = true;
const HomePage =(props)=>{
props = {
brew : DEFAULT_BREW,
ver : '0.0.0',
...props
};
...props
};
const [currentBrew , setCurrentBrew] = useState(props.brew);
const [welcomeText , setWelcomeText] = useState(props.brew.text);
const [error , setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = 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(()=>{
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
@@ -67,7 +71,7 @@ const HomePage =(props)=>{
document.addEventListener('keydown', handleControlKeys);
return () => {
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
@@ -85,20 +89,27 @@ const HomePage =(props)=>{
});
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme')
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(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
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 = ()=>{
setError(null);
setIsSaving(false);
@@ -120,10 +166,9 @@ const HomePage =(props)=>{
const renderNavbar = ()=>{
return <Navbar ver={props.ver}>
<Nav.section>
{error ?
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
null
}
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
@@ -165,7 +210,7 @@ const HomePage =(props)=>{
/>
</SplitPane>
</div>
<div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}>
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
Save current <i className='fas fa-save' />
</div>
@@ -173,7 +218,7 @@ const HomePage =(props)=>{
Create your own <i className='fas fa-magic' />
</a>
</div>
)
);
};
module.exports = HomePage;

View File

@@ -34,7 +34,13 @@
}
.navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange;
&: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 request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
@@ -26,19 +27,18 @@ import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports
import { Meta } from 'vitreum/headtags';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const METAKEY = 'HB_newPage_metadata';
const SNIPKEY = 'HB_newPage_snippets';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true;
const neverSaved = true;
const NewPage = (props) => {
const NewPage = (props)=>{
props = {
brew: DEFAULT_BREW,
brew : DEFAULT_BREW,
...props
};
@@ -51,10 +51,13 @@ const NewPage = (props) => {
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
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();
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
@@ -70,7 +73,7 @@ const NewPage = (props) => {
document.addEventListener('keydown', handleControlKeys);
return () => {
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
@@ -93,6 +96,7 @@ const NewPage = (props) => {
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew);
lastSavedBrew.current = brew;
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
localStorage.setItem(BREWKEY, brew.text);
@@ -103,20 +107,27 @@ const NewPage = (props) => {
window.history.replaceState({}, window.location.title, '/new/');
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme')
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(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
@@ -130,10 +141,10 @@ const NewPage = (props) => {
}
};
const save = async () => {
const save = async ()=>{
setIsSaving(true);
let updatedBrew = { ...currentBrew };
const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
@@ -142,13 +153,13 @@ const NewPage = (props) => {
const res = await request
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(updatedBrew)
.catch((err) => {
.catch((err)=>{
setIsSaving(false);
setError(err);
});
setIsSaving(false)
if (!res) return;
setIsSaving(false);
if(!res) return;
const savedBrew = res.body;
@@ -159,15 +170,38 @@ const NewPage = (props) => {
};
const renderSaveButton = ()=>{
if(isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
save
</Nav.item>;
}
// #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 = ()=>{
@@ -175,7 +209,7 @@ const NewPage = (props) => {
setIsSaving(false);
};
const renderNavbar = () => (
const renderNavbar = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
@@ -196,7 +230,7 @@ const NewPage = (props) => {
);
return (
<div className='newPage sitePage'>
<div className='newPage sitePage'>
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ const getLocalStorageMap = function(){
if(global?.account?.username){
const username = global.account.username;
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
}
return localStorageMap;

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
if(typeof window === 'undefined') return;
// Return if the local storage key map has no content
const localStorageKeyMap = getLocalStorageMap();
if(Object.keys(localStorageKeyMap).length == 0) return;
const storage = window.localStorage;
Object.keys(localStorageKeyMap).forEach((key)=>{

View File

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

View File

@@ -8,9 +8,9 @@ import Markdown from '../shared/naturalcrit/markdown.js';
import yaml from 'js-yaml';
import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid';
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { splitTextStyleAndMetadata,
import { splitTextStyleAndMetadata,
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js';
@@ -377,14 +377,14 @@ const api = {
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
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];
} catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
console.error('Failed to apply patches:', {
//patches : brewFromClient.patches,
brewId : brewFromClient.editId || 'unknown',
error : err
brewId : brewFromClient.editId || 'unknown',
error : err
});
// While running in parallel, don't throw the error upstream.
// throw err; // rethrow to preserve the 500 behavior

View File

@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
const mpAsSnippets = [];
// Snippets from Themes first.
if(themeBundleSnippets) {
for (let themes of themeBundleSnippets) {
for (const themes of themeBundleSnippets) {
if(typeof themes !== 'string') {
const userSnippets = [];
const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
@@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{
if(typeof yamlObj == 'string') return yamlObj;
let snippetsText = '';
for (let snippet of yamlObj) {
for (let subSnippet of snippet.subsnippets) {
for (const snippet of yamlObj) {
for (const subSnippet of snippet.subsnippets) {
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
}
}
@@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
const res = await request
.get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{
setError(err)
setError(err);
});
if(!res) {
setThemeBundle({});
@@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
setError(null);
};
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
const clientText = clientTextRaw?.normalize('NFC') || '';
const serverText = serverTextRaw?.normalize('NFC') || '';
const clientBuffer = Buffer.from(clientText, 'utf8');
const serverBuffer = Buffer.from(serverText, 'utf8');
if (clientBuffer.equals(serverBuffer)) {
if(clientBuffer.equals(serverBuffer)) {
console.log(`${label} text matches byte-for-byte.`);
return;
}
@@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Byte-level diff
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
if (clientBuffer[i] !== serverBuffer[i]) {
if(clientBuffer[i] !== serverBuffer[i]) {
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
break;
}
@@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Char-level diff
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
if (clientText[i] !== serverText[i]) {
if(clientText[i] !== serverText[i]) {
console.log(`Char mismatch at index ${i}:`);
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
break;
}
}
}
};
export {
splitTextStyleAndMetadata,

View File

@@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
try {
return mathParser.evaluate(replacedLabel);
} catch (error) {
} catch {
return undefined; // Return undefined if invalid math result
}
}
@@ -680,7 +680,7 @@ const tableTerminators = [
Marked.use(MarkedVariables());
Marked.use(MarkedDefinitionLists());
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock);
Marked.use(MarkedAlignedParagraphs());
Marked.use(MarkedSubSuperText());

View File

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

View File

@@ -611,3 +611,17 @@ h6,
}
.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;
}
}
}