mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-11 06:52:38 +00:00
Merge branch 'master' into remove-config-enable-checks
This commit is contained in:
@@ -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 */}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -325,10 +325,10 @@ const Editor = createClass({
|
|||||||
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
||||||
const currentPos = brewRenderer.scrollTop;
|
const currentPos = brewRenderer.scrollTop;
|
||||||
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
||||||
|
|
||||||
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
|
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
||||||
scrollingTimeout = setTimeout(()=>{
|
scrollingTimeout = setTimeout(()=>{
|
||||||
isJumping = false;
|
isJumping = false;
|
||||||
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
|
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
|
||||||
@@ -369,8 +369,8 @@ const Editor = createClass({
|
|||||||
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
||||||
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
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
|
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
||||||
scrollingTimeout = setTimeout(()=>{
|
scrollingTimeout = setTimeout(()=>{
|
||||||
isJumping = false;
|
isJumping = false;
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,11 +45,20 @@ const Homebrew = (props)=>{
|
|||||||
global.version = version;
|
global.version = version;
|
||||||
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} />} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ require('./error-navitem.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
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'>
|
||||||
@@ -112,6 +112,15 @@ const ErrorNavItem = ({error = '', clearError})=>{
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '13') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Server has lost connection to the database.
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
if(errorCode === 'ECONNABORTED') {
|
if(errorCode === 'ECONNABORTED') {
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ 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);
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ import React from 'react';
|
|||||||
import dedent from 'dedent-tabs';
|
import dedent from 'dedent-tabs';
|
||||||
import Nav from 'naturalcrit/nav/nav.jsx';
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
|
|
||||||
const getShareId = (brew)=>(
|
const getShareId = (brew)=>(
|
||||||
brew.googleId && !brew.stubbed
|
brew.googleId && !brew.stubbed
|
||||||
? brew.googleId + brew.shareId
|
? brew.googleId + brew.shareId
|
||||||
: brew.shareId
|
: brew.shareId
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRedditLink = (brew)=>{
|
const getRedditLink = (brew)=>{
|
||||||
const text = dedent`
|
const text = dedent`
|
||||||
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
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)})**`;
|
**[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.dropdown>
|
||||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||||
share
|
share
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -123,16 +123,16 @@ const EditPage = (props)=>{
|
|||||||
editorRef.current?.update();
|
editorRef.current?.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||||
if (subfield == 'renderer' || subfield == 'theme')
|
if(subfield == 'renderer' || subfield == 'theme')
|
||||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.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 && (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 = ()=>{
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Database Connection Lost
|
||||||
|
'13' : dedent`
|
||||||
|
## Database connection has been lost.
|
||||||
|
|
||||||
|
The server could not communicate with the database.`,
|
||||||
|
|
||||||
//account page when account is not defined
|
//account page when account is not defined
|
||||||
'50' : dedent`
|
'50' : dedent`
|
||||||
## You are not signed in
|
## You are not signed in
|
||||||
|
|||||||
@@ -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,25 +33,28 @@ 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 = {
|
||||||
brew : DEFAULT_BREW,
|
brew : DEFAULT_BREW,
|
||||||
ver : '0.0.0',
|
ver : '0.0.0',
|
||||||
...props
|
...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);
|
||||||
@@ -67,7 +71,7 @@ const HomePage =(props)=>{
|
|||||||
|
|
||||||
document.addEventListener('keydown', handleControlKeys);
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
|
|
||||||
return () => {
|
return ()=>{
|
||||||
document.removeEventListener('keydown', handleControlKeys);
|
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 = ()=>{
|
const handleSplitMove = ()=>{
|
||||||
editorRef.current.update();
|
editorRef.current.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||||
if (subfield == 'renderer' || subfield == 'theme')
|
if(subfield == 'renderer' || subfield == 'theme')
|
||||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.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 && (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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,19 +27,18 @@ 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 = {
|
||||||
brew: DEFAULT_BREW,
|
brew : DEFAULT_BREW,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,10 +51,13 @@ 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();
|
||||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||||
|
|
||||||
@@ -70,7 +73,7 @@ const NewPage = (props) => {
|
|||||||
|
|
||||||
document.addEventListener('keydown', handleControlKeys);
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
|
|
||||||
return () => {
|
return ()=>{
|
||||||
document.removeEventListener('keydown', handleControlKeys);
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -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,20 +107,27 @@ 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();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||||
if (subfield == 'renderer' || subfield == 'theme')
|
if(subfield == 'renderer' || subfield == 'theme')
|
||||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.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 && (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);
|
||||||
@@ -130,10 +141,10 @@ 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;
|
||||||
@@ -142,13 +153,13 @@ const NewPage = (props) => {
|
|||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||||
.send(updatedBrew)
|
.send(updatedBrew)
|
||||||
.catch((err) => {
|
.catch((err)=>{
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
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 = ()=>{
|
||||||
@@ -175,7 +209,7 @@ const NewPage = (props) => {
|
|||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderNavbar = () => (
|
const renderNavbar = ()=>(
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||||
@@ -196,7 +230,7 @@ const NewPage = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='newPage sitePage'>
|
<div className='newPage sitePage'>
|
||||||
{renderNavbar()}
|
{renderNavbar()}
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={handleSplitMove}>
|
<SplitPane onDragFinish={handleSplitMove}>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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)=>{
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
722
package-lock.json
generated
722
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -138,15 +138,15 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^4.0.0",
|
"@stylistic/stylelint-plugin": "^4.0.0",
|
||||||
"babel-plugin-transform-import-meta": "^2.3.3",
|
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||||
"eslint": "^9.35.0",
|
"eslint": "^9.37.0",
|
||||||
"eslint-plugin-jest": "^29.0.1",
|
"eslint-plugin-jest": "^29.0.1",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^16.3.0",
|
"globals": "^16.4.0",
|
||||||
"jest": "^30.1.3",
|
"jest": "^30.2.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.24.0",
|
"stylelint": "^16.25.0",
|
||||||
"stylelint-config-recess-order": "^7.3.0",
|
"stylelint-config-recess-order": "^7.3.0",
|
||||||
"stylelint-config-recommended": "^17.0.0",
|
"stylelint-config-recommended": "^17.0.0",
|
||||||
"supertest": "^7.1.4"
|
"supertest": "^7.1.4"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import contentNegotiation from './middleware/content-negotiation.js';
|
|||||||
import bodyParser from 'body-parser';
|
import bodyParser from 'body-parser';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import forceSSL from './forcessl.mw.js';
|
import forceSSL from './forcessl.mw.js';
|
||||||
|
import dbCheck from './middleware/dbCheck.js';
|
||||||
|
|
||||||
|
|
||||||
const sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
@@ -274,7 +275,7 @@ app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
|
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
|
||||||
|
|
||||||
//User Page
|
//User Page
|
||||||
app.get('/user/:username', async (req, res, next)=>{
|
app.get('/user/:username', dbCheck, async (req, res, next)=>{
|
||||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -346,7 +347,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
//Change author name on brews
|
//Change author name on brews
|
||||||
app.put('/api/user/rename', async (req, res)=>{
|
app.put('/api/user/rename', dbCheck, async (req, res)=>{
|
||||||
const { username, newUsername } = req.body;
|
const { username, newUsername } = req.body;
|
||||||
const ownAccount = req.account && (req.account.username == newUsername);
|
const ownAccount = req.account && (req.account.username == newUsername);
|
||||||
|
|
||||||
@@ -432,7 +433,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
//Share Page
|
//Share Page
|
||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
const { brew } = req;
|
const { brew } = req;
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||||
@@ -459,7 +460,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
//Account Page
|
//Account Page
|
||||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{
|
||||||
const data = {};
|
const data = {};
|
||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
|
|||||||
12
server/db.js
12
server/db.js
@@ -22,6 +22,14 @@ const handleConnectionError = (error)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addListeners = (conn)=>{
|
||||||
|
conn.connection.on('disconnecting', ()=>{console.log('Mongo disconnecting...');});
|
||||||
|
conn.connection.on('disconnected', ()=>{console.log('Mongo disconnected!');});
|
||||||
|
conn.connection.on('connecting', ()=>{console.log('Mongo connecting...');});
|
||||||
|
conn.connection.on('connected', ()=>{console.log('Mongo connected!');});
|
||||||
|
return conn;
|
||||||
|
};
|
||||||
|
|
||||||
const disconnect = async ()=>{
|
const disconnect = async ()=>{
|
||||||
return await Mongoose.disconnect();
|
return await Mongoose.disconnect();
|
||||||
};
|
};
|
||||||
@@ -31,10 +39,12 @@ const connect = async (config)=>{
|
|||||||
retryWrites : false,
|
retryWrites : false,
|
||||||
autoIndex : (config.get('local_environments').includes(config.get('node_env')))
|
autoIndex : (config.get('local_environments').includes(config.get('node_env')))
|
||||||
})
|
})
|
||||||
.catch((error)=>handleConnectionError(error));
|
.then(addListeners(Mongoose))
|
||||||
|
.catch((error)=>handleConnectionError(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
connect,
|
connect,
|
||||||
disconnect
|
disconnect
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import { nanoid } from 'nanoid';
|
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 { md5 } from 'hash-wasm';
|
||||||
import { splitTextStyleAndMetadata,
|
import { splitTextStyleAndMetadata,
|
||||||
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
||||||
import checkClientVersion from './middleware/check-client-version.js';
|
import checkClientVersion from './middleware/check-client-version.js';
|
||||||
|
import dbCheck from './middleware/dbCheck.js';
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -377,14 +378,14 @@ 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}`);
|
||||||
console.error('Failed to apply patches:', {
|
console.error('Failed to apply patches:', {
|
||||||
//patches : brewFromClient.patches,
|
//patches : brewFromClient.patches,
|
||||||
brewId : brewFromClient.editId || 'unknown',
|
brewId : brewFromClient.editId || 'unknown',
|
||||||
error : err
|
error : err
|
||||||
});
|
});
|
||||||
// While running in parallel, don't throw the error upstream.
|
// While running in parallel, don't throw the error upstream.
|
||||||
// throw err; // rethrow to preserve the 500 behavior
|
// throw err; // rethrow to preserve the 500 behavior
|
||||||
@@ -530,6 +531,8 @@ const api = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
router.use(dbCheck);
|
||||||
|
|
||||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||||
|
|||||||
15
server/middleware/dbCheck.js
Normal file
15
server/middleware/dbCheck.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import config from '../config.js';
|
||||||
|
|
||||||
|
export default (req, res, next)=>{
|
||||||
|
// Bypass DB checks during testing
|
||||||
|
if(config.get('node_env') == 'test') return next();
|
||||||
|
|
||||||
|
if(mongoose.connection.readyState == 1) return next();
|
||||||
|
throw {
|
||||||
|
HBErrorCode : '13',
|
||||||
|
name : 'Database Connection Error',
|
||||||
|
message : 'Unable to connect to database',
|
||||||
|
status : mongoose.connection.readyState
|
||||||
|
};
|
||||||
|
};
|
||||||
28
server/middleware/dbCheck.spec.js
Normal file
28
server/middleware/dbCheck.spec.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import dbCheck from './dbCheck.js';
|
||||||
|
import config from '../config.js';
|
||||||
|
|
||||||
|
describe('dbCheck middleware', ()=>{
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
afterEach(()=>jest.clearAllMocks());
|
||||||
|
|
||||||
|
it('should skip check in test mode', ()=>{
|
||||||
|
config.get = jest.fn(()=>'test');
|
||||||
|
expect(()=>dbCheck({}, {}, next)).not.toThrow();
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next if readyState == 1', ()=>{
|
||||||
|
config.get = jest.fn(()=>'production');
|
||||||
|
mongoose.connection.readyState = 1;
|
||||||
|
dbCheck({}, {}, next);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if readyState != 1', ()=>{
|
||||||
|
config.get = jest.fn(()=>'production');
|
||||||
|
mongoose.connection.readyState = 99;
|
||||||
|
expect(()=>dbCheck({}, {}, next)).toThrow(/Unable to connect/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
@@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{
|
|||||||
if(typeof yamlObj == 'string') return yamlObj;
|
if(typeof yamlObj == 'string') return 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({});
|
||||||
@@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
|
|||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
|
||||||
const clientText = clientTextRaw?.normalize('NFC') || '';
|
const clientText = clientTextRaw?.normalize('NFC') || '';
|
||||||
const serverText = serverTextRaw?.normalize('NFC') || '';
|
const serverText = serverTextRaw?.normalize('NFC') || '';
|
||||||
|
|
||||||
const clientBuffer = Buffer.from(clientText, 'utf8');
|
const clientBuffer = Buffer.from(clientText, 'utf8');
|
||||||
const serverBuffer = Buffer.from(serverText, 'utf8');
|
const serverBuffer = Buffer.from(serverText, 'utf8');
|
||||||
|
|
||||||
if (clientBuffer.equals(serverBuffer)) {
|
if(clientBuffer.equals(serverBuffer)) {
|
||||||
console.log(`✅ ${label} text matches byte-for-byte.`);
|
console.log(`✅ ${label} text matches byte-for-byte.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
|||||||
|
|
||||||
// Byte-level diff
|
// Byte-level diff
|
||||||
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
|
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)}`);
|
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
|||||||
|
|
||||||
// Char-level diff
|
// Char-level diff
|
||||||
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
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(`Char mismatch at index ${i}:`);
|
||||||
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||||
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
splitTextStyleAndMetadata,
|
splitTextStyleAndMetadata,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,7 +680,7 @@ const tableTerminators = [
|
|||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use(MarkedDefinitionLists());
|
Marked.use(MarkedDefinitionLists());
|
||||||
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use(MarkedAlignedParagraphs());
|
Marked.use(MarkedAlignedParagraphs());
|
||||||
Marked.use(MarkedSubSuperText());
|
Marked.use(MarkedSubSuperText());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user