0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-03 23:32:58 +00:00

Merge branch 'master' into addDBCheckMiddleware

This commit is contained in:
G.Ambatte
2025-10-06 16:48:37 +13:00
committed by GitHub
15 changed files with 153 additions and 73 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

@@ -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

@@ -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 = {
@@ -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();
}; };
@@ -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>

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();
}; };
@@ -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

@@ -26,7 +26,7 @@ const getLocalStorageMap = function(){
if(global?.account?.username){ if(global?.account?.username){
const username = 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; 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 // 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

@@ -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;
}
}
}