0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-08 22:32:41 +00:00

Merge branch 'more-style-snippets' of https://github.com/5e-Cleric/homebrewery into more-style-snippets

This commit is contained in:
Víctor Losada Hernández
2024-09-16 22:20:54 +02:00
37 changed files with 2491 additions and 973 deletions

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
package-lock.json binary package-lock.json binary
*.json text eol=lf

36
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,36 @@
<!--
Before submitting a Pull Request, please consider the following to speed up reviews:
- 👷‍♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
-->
## Description
## Related Issues or Discussions
- Closes #
## QA Instructions, Screenshots, Recordings
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
images for UI changes._
### Reviewer Checklist
_Please replace the list below with specific features you want reviewers to look at._
*Reviewers, refer to this list when testing features, or suggest new items *
- [ ] Verify new features are functional
- [ ] Feature A does X
- [ ] Feature B does Y
- [ ] Verify old features have not broken
- [ ] Feature Z can still be used
- [ ] Test for edge cases / try to break things
- [ ] Feature A handles negative numbers
- [ ] Identify opportunities for simplification and refactoring
- [ ] Check for code legibility and appropriate comments
<details><summary>Copy this list</summary>

View File

@@ -84,9 +84,54 @@ pre {
## changelog ## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Tuesday 8/27/2024 - v3.14.2 ### Wednesday 9/04/2024 - v3.15.0
{{taskList
{{taskList
##### 5e-Cleric, abquintic, calculuschild, Gazook89, G-Ambatte, Ericsheid, Kaiburr
* [x] New {{openSans **VAULT** {{fas,fa-dungeon}}}} page 🎉🎉🎉
:
All **PUBLISHED** brews ({{openSans :fas_circle_info: **Properties**}} menu) will be searchable, by title or author, and filtered by renderer. More features and adjustments will be coming.
:
Note: If any of your own brews are not showing up in search (particularly if stored on Google Drive), please edit and re-save to ensure our database has the data needed from document to be searchable.
Fixes issue [#697](https://github.com/naturalcrit/homebrewery/issues/697)
##### Gazook89
* [x] Auto-focus on text editor when switching editor tabs
}}
### Wednesday 8/28/2024 - v3.14.3
{{taskList
##### calculuschild, G-Ambatte
* [x] New {{openSans **IMAGES → {{fac,image-wrap-left}} IMAGE WRAP LEFT/RIGHT**}} snippets
Fixes issue [#380](https://github.com/naturalcrit/homebrewery/issues/380)
* [x] Fix v3.14.2 bug with `` failing after tables
##### 5e-Cleric
* [x] Fix Account page crash when not logged in
Fixes issue [#3605](https://github.com/naturalcrit/homebrewery/issues/3605)
##### abquintic
* [x] Fix jump hotkeys conflicting with `CTRL + SHIFT`. Preview and Source movement shortcuts now use `CTRL + SHIFT + META + LEFT\RIGHTARROW`
##### G-Ambatte
* [x] Fix display issue with image wrap icons
}}
### Tuesday 8/27/2024 - v3.14.2
{{taskList
##### calculuschild ##### calculuschild
* [x] Reroute invalid urls to homepage * [x] Reroute invalid urls to homepage
@@ -119,7 +164,7 @@ Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
Fixes issues [#1430](https://github.com/naturalcrit/homebrewery/issues/1430) Fixes issues [#1430](https://github.com/naturalcrit/homebrewery/issues/1430)
* [x] Fix colon `:::` being parsed in codeblocks * [x] Fix colon `` being parsed in codeblocks
* [x] Prevent crashes when loading undefined renderer or theme bundle * [x] Prevent crashes when loading undefined renderer or theme bundle
@@ -133,12 +178,11 @@ Fixes issues [#1430](https://github.com/naturalcrit/homebrewery/issues/1430)
##### 5e-Cleric, Gazook89 ##### 5e-Cleric, Gazook89
* [x] Viewer tools for zoom/page navigation * [x] Viewer tools for zoom/page navigation
}} }}
### Tuesday 8/13/2024 - v3.14.1 ### Tuesday 8/13/2024 - v3.14.1
{{taskList
{{taskList
##### abquintic ##### abquintic
* [x] Allow Table of Contents to flow across columns * [x] Allow Table of Contents to flow across columns
@@ -181,16 +225,13 @@ Fixes issues [#3613](https://github.com/naturalcrit/homebrewery/issues/3613)
Fixes issues [#3622](https://github.com/naturalcrit/homebrewery/issues/3622) Fixes issues [#3622](https://github.com/naturalcrit/homebrewery/issues/3622)
##### calculuschild ##### calculuschild
* [x] Fix `/migrate` page using an editor context instead of share context * [x] Fix `/migrate` page using an editor context instead of share context
##### 5e-Cleric ##### 5e-Cleric
* [x] Fix Monster Stat Blocks losing color in Safari * [x] Fix Monster Stat Blocks losing color in Safari
}} }}
\page \page

View File

@@ -1,7 +1,7 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less'); require('./brewRenderer.less');
const React = require('react'); const React = require('react');
const { useState, useRef, useEffect } = React; const { useState, useRef, useEffect, useCallback } = React;
const _ = require('lodash'); const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
@@ -49,23 +49,25 @@ let rawPages = [];
const BrewRenderer = (props)=>{ const BrewRenderer = (props)=>{
props = { props = {
text : '', text : '',
style : '', style : '',
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB', theme : '5ePHB',
lang : '', lang : '',
errors : [], errors : [],
currentEditorPage : 0, currentEditorCursorPageNum : 0,
themeBundle : {}, currentEditorViewPageNum : 0,
currentBrewRendererPageNum : 0,
themeBundle : {},
onPageChange : ()=>{},
...props ...props
}; };
const [state, setState] = useState({ const [state, setState] = useState({
height : PAGE_HEIGHT, height : PAGE_HEIGHT,
isMounted : false, isMounted : false,
visibility : 'hidden', visibility : 'hidden',
zoom : 100, zoom : 100
currentPageNumber : 1,
}); });
const mainRef = useRef(null); const mainRef = useRef(null);
@@ -87,25 +89,22 @@ const BrewRenderer = (props)=>{
})); }));
}; };
const getCurrentPage = (e)=>{ const updateCurrentPage = useCallback(_.throttle((e)=>{
const { scrollTop, clientHeight, scrollHeight } = e.target; const { scrollTop, clientHeight, scrollHeight } = e.target;
const totalScrollableHeight = scrollHeight - clientHeight; const totalScrollableHeight = scrollHeight - clientHeight;
const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length); const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
setState((prevState)=>({ props.onPageChange(currentPageNumber);
...prevState, }, 200), []);
currentPageNumber : currentPageNumber || 1
}));
};
const isInView = (index)=>{ const isInView = (index)=>{
if(!state.isMounted) if(!state.isMounted)
return false; return false;
if(index == props.currentEditorPage) //Already rendered before this step if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step
return false; return false;
if(Math.abs(index - state.currentPageNumber) <= 3) if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3)
return true; return true;
return false; return false;
@@ -142,7 +141,7 @@ const BrewRenderer = (props)=>{
renderedPages.length = 0; renderedPages.length = 0;
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first // Render currently-edited page first so cross-page effects (variables, links) can propagate out first
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage); renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
_.forEach(rawPages, (page, index)=>{ _.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
@@ -192,7 +191,7 @@ const BrewRenderer = (props)=>{
<> <>
{/*render dummy page while iFrame is mounting.*/} {/*render dummy page while iFrame is mounting.*/}
{!state.isMounted {!state.isMounted
? <div className='brewRenderer' onScroll={getCurrentPage}> ? <div className='brewRenderer' onScroll={updateCurrentPage}>
<div className='pages'> <div className='pages'>
{renderDummyPage(1)} {renderDummyPage(1)}
</div> </div>
@@ -205,7 +204,7 @@ const BrewRenderer = (props)=>{
<NotificationPopup /> <NotificationPopup />
</div> </div>
<ToolBar onZoomChange={handleZoom} currentPage={state.currentPageNumber} totalPages={rawPages.length}/> <ToolBar onZoomChange={handleZoom} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length}/>
{/*render in iFrame so broken code doesn't crash the site.*/} {/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT} <Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
@@ -214,7 +213,7 @@ const BrewRenderer = (props)=>{
onClick={()=>{emitClick();}} onClick={()=>{emitClick();}}
> >
<div className={'brewRenderer'} <div className={'brewRenderer'}
onScroll={getCurrentPage} onScroll={updateCurrentPage}
onKeyDown={handleControlKeys} onKeyDown={handleControlKeys}
tabIndex={-1} tabIndex={-1}
style={{ height: state.height }}> style={{ height: state.height }}>

View File

@@ -4,7 +4,7 @@ const _ = require('lodash');
import Dialog from '../../../components/dialog.jsx'; import Dialog from '../../../components/dialog.jsx';
const DISMISS_KEY = 'dismiss_notification12-04-23'; const DISMISS_KEY = 'dismiss_notification04-09-24';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />; const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
const NotificationPopup = ()=>{ const NotificationPopup = ()=>{
@@ -15,11 +15,12 @@ const NotificationPopup = ()=>{
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small> <small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
</div> </div>
<ul> <ul>
<li key='psa'> <li key='Vault'>
<em>Don't store IMAGES in Google Drive</em><br /> <em>Search brews with our new page!</em><br />
Google Drive is not an image service, and will block images from being used We have been working very hard in making this possible, now you can share your work and look at it in the new <a href='/vault'>Vault</a> page!
in brews if they get more views than expected. Google has confirmed they won't fix All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
More features will be coming.
</li> </li>
<li key='googleDriveFolder'> <li key='googleDriveFolder'>

View File

@@ -11,6 +11,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
const [zoomLevel, setZoomLevel] = useState(100); const [zoomLevel, setZoomLevel] = useState(100);
const [pageNum, setPageNum] = useState(currentPage); const [pageNum, setPageNum] = useState(currentPage);
const [toolsVisible, setToolsVisible] = useState(true);
useEffect(()=>{ useEffect(()=>{
onZoomChange(zoomLevel); onZoomChange(zoomLevel);
@@ -55,7 +56,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
} else if(mode == 'fit'){ } else if(mode == 'fit'){
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it. // find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
const minDimRatio = [...pages].reduce((minRatio, page) => Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity); const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
desiredZoom = minDimRatio * 100; desiredZoom = minDimRatio * 100;
} }
@@ -67,7 +68,8 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{
}; };
return ( return (
<div className='toolBar'> <div className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`}>
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
{/*v=====----------------------< Zoom Controls >---------------------=====v*/} {/*v=====----------------------< Zoom Controls >---------------------=====v*/}
<div className='group'> <div className='group'>
<button <button

View File

@@ -15,6 +15,10 @@
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
color : #CCCCCC; color : #CCCCCC;
background-color : #555555; background-color : #555555;
& > *:not(.toggleButton) {
opacity: 1;
transition: all .2s ease;
}
.group { .group {
box-sizing : border-box; box-sizing : border-box;
@@ -100,4 +104,25 @@
font-size:1.2em; font-size:1.2em;
} }
} }
&.hidden {
width: 32px;
transition: all .3s ease;
flex-wrap:nowrap;
overflow: hidden;
background-color: unset;
opacity: .5;
& > *:not(.toggleButton) {
opacity: 0;
transition: all .2s ease;
}
}
}
button.toggleButton {
z-index : 5;
position:absolute;
left: 0;
width: 32px;
min-width: unset;
} }

View File

@@ -1,9 +1,8 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
require('./editor.less'); require('./editor.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const Markdown = require('../../../shared/naturalcrit/markdown.js'); const Markdown = require('../../../shared/naturalcrit/markdown.js');
@@ -22,6 +21,7 @@ const DEFAULT_STYLE_TEXT = dedent`
color: black; color: black;
}`; }`;
let isJumping = false;
const Editor = createClass({ const Editor = createClass({
displayName : 'Editor', displayName : 'Editor',
@@ -37,8 +37,15 @@ const Editor = createClass({
onMetaChange : ()=>{}, onMetaChange : ()=>{},
reportError : ()=>{}, reportError : ()=>{},
onCursorPageChange : ()=>{},
onViewPageChange : ()=>{},
editorTheme : 'default', editorTheme : 'default',
renderer : 'legacy' renderer : 'legacy',
currentEditorCursorPageNum : 1,
currentEditorViewPageNum : 1,
currentBrewRendererPageNum : 1,
}; };
}, },
getInitialState : function() { getInitialState : function() {
@@ -56,12 +63,16 @@ const Editor = createClass({
isMeta : function() {return this.state.view == 'meta';}, isMeta : function() {return this.state.view == 'meta';},
componentDidMount : function() { componentDidMount : function() {
this.updateEditorSize(); this.updateEditorSize();
this.highlightCustomMarkdown(); this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize); window.addEventListener('resize', this.updateEditorSize);
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys); document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY); const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) { if(editorTheme) {
this.setState({ this.setState({
@@ -75,28 +86,37 @@ const Editor = createClass({
}, },
componentDidUpdate : function(prevProps, prevState, snapshot) { componentDidUpdate : function(prevProps, prevState, snapshot) {
this.highlightCustomMarkdown(); this.highlightCustomMarkdown();
if(prevProps.moveBrew !== this.props.moveBrew) { if(prevProps.moveBrew !== this.props.moveBrew)
this.brewJump(); this.brewJump();
};
if(prevProps.moveSource !== this.props.moveSource) { if(prevProps.moveSource !== this.props.moveSource)
this.sourceJump(); this.sourceJump();
};
if(this.props.liveScroll) {
if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) {
this.sourceJump(this.props.currentBrewRendererPageNum, false);
} else if(prevProps.currentEditorViewPageNum !== this.props.currentEditorViewPageNum) {
this.brewJump(this.props.currentEditorViewPageNum, false);
} else if(prevProps.currentEditorCursorPageNum !== this.props.currentEditorCursorPageNum) {
this.brewJump(this.props.currentEditorCursorPageNum, false);
}
}
}, },
handleControlKeys : function(e){ handleControlKeys : function(e){
if(!(e.ctrlKey && e.metaKey)) return; if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
const LEFTARROW_KEY = 37; const LEFTARROW_KEY = 37;
const RIGHTARROW_KEY = 39; const RIGHTARROW_KEY = 39;
if (e.shiftKey && (e.keyCode == RIGHTARROW_KEY)) this.brewJump(); if(e.keyCode == RIGHTARROW_KEY) this.brewJump();
if (e.shiftKey && (e.keyCode == LEFTARROW_KEY)) this.sourceJump(); if(e.keyCode == LEFTARROW_KEY) this.sourceJump();
if ((e.keyCode == LEFTARROW_KEY) || (e.keyCode == RIGHTARROW_KEY)) { if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} }
}, },
updateEditorSize : function() { updateEditorSize : function() {
if(this.codeEditor.current) { if(this.codeEditor.current) {
let paneHeight = this.editor.current.parentNode.clientHeight; let paneHeight = this.editor.current.parentNode.clientHeight;
@@ -105,6 +125,20 @@ const Editor = createClass({
} }
}, },
updateCurrentCursorPage : function(cursor) {
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onCursorPageChange(currentPage);
},
updateCurrentViewPage : function(topScrollLine) {
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
this.props.onViewPageChange(currentPage);
},
handleInject : function(injectText){ handleInject : function(injectText){
this.codeEditor.current?.injectText(injectText, false); this.codeEditor.current?.injectText(injectText, false);
}, },
@@ -113,19 +147,10 @@ const Editor = createClass({
this.props.setMoveArrows(newView === 'text'); this.props.setMoveArrows(newView === 'text');
this.setState({ this.setState({
view : newView view : newView
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed }, ()=>{
}, this.codeEditor.current?.codeMirror.focus();
this.updateEditorSize();
getCurrentPage : function(){ }); //TODO: not sure if updateeditorsize needed
const lines = this.props.brew.text.split('\n').slice(0, this.codeEditor.current.getCursorPosition().line + 1);
return _.reduce(lines, (r, line)=>{
if(
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
||
(this.props.renderer == 'V3' && line.match(/^\\page$/))
) r++;
return r;
}, 1);
}, },
highlightCustomMarkdown : function(){ highlightCustomMarkdown : function(){
@@ -136,13 +161,13 @@ const Editor = createClass({
codeMirror.operation(()=>{ // Batch CodeMirror styling codeMirror.operation(()=>{ // Batch CodeMirror styling
const foldLines = []; const foldLines = [];
//reset custom text styles //reset custom text styles
const customHighlights = codeMirror.getAllMarks().filter((mark)=>{ const customHighlights = codeMirror.getAllMarks().filter((mark)=>{
// Record details of folded sections // Record details of folded sections
if(mark.__isFold) { if(mark.__isFold) {
const fold = mark.find(); const fold = mark.find();
foldLines.push({from: fold.from?.line, to: fold.to?.line}); foldLines.push({ from: fold.from?.line, to: fold.to?.line });
} }
return !mark.__isFold; return !mark.__isFold;
}); //Don't undo code folding }); //Don't undo code folding
@@ -160,7 +185,7 @@ const Editor = createClass({
// Don't process lines inside folded text // Don't process lines inside folded text
// If the current lineNumber is inside any folded marks, skip line styling // If the current lineNumber is inside any folded marks, skip line styling
if (foldLines.some(fold => lineNumber >= fold.from && lineNumber <= fold.to)) if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
return; return;
// Styling for \page breaks // Styling for \page breaks
@@ -186,7 +211,7 @@ const Editor = createClass({
// definition lists // definition lists
if(line.includes('::')){ if(line.includes('::')){
if(/^:*$/.test(line) == true){ return }; if(/^:*$/.test(line) == true){ return; };
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error. const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
let match; let match;
while ((match = regex.exec(line)) != null){ while ((match = regex.exec(line)) != null){
@@ -194,10 +219,10 @@ const Editor = createClass({
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' }); codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' }); codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
const ddIndex = match.indices[2][0]; const ddIndex = match.indices[2][0];
let colons = /::/g; const colons = /::/g;
let colonMatches = colons.exec(match[2]); const colonMatches = colons.exec(match[2]);
if(colonMatches !== null){ if(colonMatches !== null){
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} ) codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
} }
} }
} }
@@ -207,12 +232,12 @@ const Editor = createClass({
let startIndex = line.indexOf('^'); let startIndex = line.indexOf('^');
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy; const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy; const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
while (startIndex >= 0) { while (startIndex >= 0) {
superRegex.lastIndex = subRegex.lastIndex = startIndex; superRegex.lastIndex = subRegex.lastIndex = startIndex;
let isSuper = false; let isSuper = false;
let match = subRegex.exec(line) || superRegex.exec(line); const match = subRegex.exec(line) || superRegex.exec(line);
if (match) { if(match) {
isSuper = !subRegex.lastIndex; isSuper = !subRegex.lastIndex;
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' }); codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
} }
@@ -262,18 +287,18 @@ const Editor = createClass({
while (startIndex >= 0) { while (startIndex >= 0) {
emojiRegex.lastIndex = startIndex; emojiRegex.lastIndex = startIndex;
let match = emojiRegex.exec(line); const match = emojiRegex.exec(line);
if (match) { if(match) {
let tokens = Markdown.marked.lexer(match[0]); let tokens = Markdown.marked.lexer(match[0]);
tokens = tokens[0].tokens.filter(t => t.type == 'emoji') tokens = tokens[0].tokens.filter((t)=>t.type == 'emoji');
if (!tokens.length) if(!tokens.length)
return; return;
let startPos = { line: lineNumber, ch: match.index }; const startPos = { line: lineNumber, ch: match.index };
let endPos = { line: lineNumber, ch: match.index + match[0].length }; const endPos = { line: lineNumber, ch: match.index + match[0].length };
// Iterate over conflicting marks and clear them // Iterate over conflicting marks and clear them
var marks = codeMirror.findMarks(startPos, endPos); const marks = codeMirror.findMarks(startPos, endPos);
marks.forEach(function(marker) { marks.forEach(function(marker) {
if(!marker.__isFold) marker.clear(); if(!marker.__isFold) marker.clear();
}); });
@@ -288,75 +313,93 @@ const Editor = createClass({
} }
}, },
brewJump : function(targetPage=this.getCurrentPage()){ brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
if(!window) return; if(!window || isJumping)
// console.log(`Scroll to: p${targetPage}`); return;
// Get current brewRenderer scroll position and calculate target position
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 interimPos = targetPos >= 0 ? -30 : 30;
const bounceDelay = 100; const checkIfScrollComplete = ()=>{
const scrollDelay = 500; let scrollingTimeout;
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
if(!this.throttleBrewMove) { scrollingTimeout = setTimeout(()=>{
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{ isJumping = false;
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' }); brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
setTimeout(()=>{ }, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay);
}, scrollDelay, { leading: true, trailing: false });
}; };
this.throttleBrewMove(currentPos, interimPos, targetPos);
// const hashPage = (page != 1) ? `p${page}` : ''; isJumping = true;
// window.location.hash = hashPage; checkIfScrollComplete();
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
if(smooth) {
const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling
const bounceDelay = 100;
const scrollDelay = 500;
if(!this.throttleBrewMove) {
this.throttleBrewMove = _.throttle((currentPos, bouncePos, targetPos)=>{
brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: 'smooth' });
setTimeout(()=>{
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
}, bounceDelay);
}, scrollDelay, { leading: true, trailing: false });
};
this.throttleBrewMove(currentPos, bouncePos, targetPos);
} else {
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' });
}
}, },
sourceJump : function(targetLine=null){ sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
if(this.isText()) { if(!this.isText || isJumping)
if(targetLine == null) { return;
targetLine = 0;
const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page'); const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height; const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
let currentPage = 1; let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
for (const page of pageCollection) { let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) {
currentPage = parseInt(page.id.slice(1)) || 1; const checkIfScrollComplete = ()=>{
break; let scrollingTimeout;
} clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done
};
isJumping = true;
checkIfScrollComplete();
this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
if(smooth) {
//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
} }
}, 10);
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/; } else {
const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-1).join(textSplit); this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
const textPosition = textString.length; this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0; this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
//Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll);
}
}, 10);
}
} }
}, },
@@ -457,7 +500,9 @@ const Editor = createClass({
currentEditorTheme={this.state.editorTheme} currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme} updateEditorTheme={this.updateEditorTheme}
snippetBundle={this.props.snippetBundle} snippetBundle={this.props.snippetBundle}
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} /> cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
updateBrew={this.props.updateBrew}
/>
{this.renderEditor()} {this.renderEditor()}
</div> </div>

View File

@@ -5,6 +5,8 @@ const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
import { getHistoryItems, historyExists } from '../../utils/versionHistory.js';
//Import all themes //Import all themes
const ThemeSnippets = {}; const ThemeSnippets = {};
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js'); ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
@@ -38,7 +40,8 @@ const Snippetbar = createClass({
unfoldCode : ()=>{}, unfoldCode : ()=>{},
updateEditorTheme : ()=>{}, updateEditorTheme : ()=>{},
cursorPos : {}, cursorPos : {},
snippetBundle : [] snippetBundle : [],
updateBrew : ()=>{}
}; };
}, },
@@ -46,7 +49,8 @@ const Snippetbar = createClass({
return { return {
renderer : this.props.renderer, renderer : this.props.renderer,
themeSelector : false, themeSelector : false,
snippets : [] snippets : [],
historyExists : false
}; };
}, },
@@ -59,18 +63,20 @@ const Snippetbar = createClass({
componentDidUpdate : async function(prevProps) { componentDidUpdate : async function(prevProps) {
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) { if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
const snippets = this.compileSnippets();
this.setState({ this.setState({
snippets : snippets snippets : this.compileSnippets()
}); });
} };
},
this.setState({
historyExists : historyExists(this.props.brew)
});
},
mergeCustomizer : function(oldValue, newValue, key) { mergeCustomizer : function(oldValue, newValue, key) {
if(key == 'snippets') { if(key == 'snippets') {
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property. return result.filter((snip)=>snip.gen || snip.subsnippets);
} }
}, },
@@ -138,6 +144,36 @@ const Snippetbar = createClass({
}); });
}, },
replaceContent : function(item){
return this.props.updateBrew(item);
},
renderHistoryItems : function() {
const historyItems = getHistoryItems(this.props.brew);
return <div className='dropdown'>
{_.map(historyItems, (item, index)=>{
if(!item.savedAt) return;
const saveTime = new Date(item.savedAt);
const diffMs = new Date() - saveTime;
const diffSecs = Math.floor(diffMs / 1000);
let diffString = `about ${diffSecs} seconds ago`;
if(diffSecs > 60) diffString = `about ${Math.floor(diffSecs / 60)} minutes ago`;
if(diffSecs > (60 * 60)) diffString = `about ${Math.floor(diffSecs / (60 * 60))} hours ago`;
if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`;
if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`;
return <div className='snippet' key={index} onClick={()=>{this.replaceContent(item);}} >
<i className={`fas fa-${index+1}`} />
<span className='name' title={saveTime.toISOString()}>v{item.version} : {diffString}</span>
</div>;
})}
</div>;
},
renderEditorButtons : function(){ renderEditorButtons : function(){
if(!this.props.showEditButtons) return; if(!this.props.showEditButtons) return;
@@ -158,6 +194,10 @@ const Snippetbar = createClass({
} }
return <div className='editors'> return <div className='editors'>
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`} >
<i className='fas fa-clock-rotate-left' />
{this.state.historyExists && this.renderHistoryItems() }
</div>
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`} <div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
onClick={this.props.undo} > onClick={this.props.undo} >
<i className='fas fa-undo' /> <i className='fas fa-undo' />

View File

@@ -53,6 +53,21 @@
font-size : 0.75em; font-size : 0.75em;
color : inherit; color : inherit;
} }
&.history {
.tooltipLeft('History');
font-size : 0.75em;
color : grey;
position : relative;
&.active {
color : inherit;
}
&>.dropdown{
right : -1px;
&>.snippet{
padding-right : 10px;
}
}
}
&.editorTheme { &.editorTheme {
.tooltipLeft('Editor Themes'); .tooltipLeft('Editor Themes');
font-size : 0.75em; font-size : 0.75em;

View File

@@ -10,6 +10,7 @@ const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx'); const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx'); const NewPage = require('./pages/newPage/newPage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx'); const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx'); const AccountPage = require('./pages/accountPage/accountPage.jsx');
const WithRoute = (props)=>{ const WithRoute = (props)=>{
@@ -71,6 +72,7 @@ const Homebrew = createClass({
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} /> <Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } /> <Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} /> <Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} /> <Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} /> <Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} /> <Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />

View File

@@ -111,7 +111,7 @@ const ErrorNavItem = createClass({
Looks like there was a problem retreiving Looks like there was a problem retreiving
the theme, or a theme that it inherits, the theme, or a theme that it inherits,
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}> for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
{response.body.brewId}</a> still exists! {response.body.brewId}</a> still exists!
</div> </div>
</Nav.item>; </Nav.item>;
} }

View File

@@ -0,0 +1,17 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function (props) {
return (
<Nav.item
color='purple'
icon='fas fa-dungeon'
href='/vault'
newTab={false}
rel='noopener noreferrer'
>
Vault
</Nav.item>
);
};

View File

@@ -19,7 +19,8 @@ const BrewItem = createClass({
stubbed : true stubbed : true
}, },
updateListFilter : ()=>{}, updateListFilter : ()=>{},
reportError : ()=>{} reportError : ()=>{},
renderStorage : true
}; };
}, },
@@ -95,6 +96,7 @@ const BrewItem = createClass({
}, },
renderStorageIcon : function(){ renderStorageIcon : function(){
if(!this.props.renderStorage) return;
if(this.props.brew.googleId) { if(this.props.brew.googleId) {
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}> return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
<a href={this.props.brew.webViewLink} target='_blank'> <a href={this.props.brew.webViewLink} target='_blank'>
@@ -142,10 +144,14 @@ const BrewItem = createClass({
} }
<span title={`Authors:\n${brew.authors?.join('\n')}`}> <span title={`Authors:\n${brew.authors?.join('\n')}`}>
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>( <i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
<> <React.Fragment key={index}>
<a key={index} href={`/user/${author}`}>{author}</a> {author === 'hidden'
{index < brew.authors.length - 1 && ', '} ? <span title="Username contained an email address; hidden to protect user's privacy">{author}</span>
</>))} : <a href={`/user/${author}`}>{author}</a>
}
{index < brew.authors.length - 1 && ', '}
</React.Fragment>
))}
</span> </span>
<br /> <br />
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}> <span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>

View File

@@ -1,8 +1,9 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
require('./editPage.less'); require('./editPage.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const createClass = require('create-react-class');
const request = require('../../utils/request-middleware.js'); const request = require('../../utils/request-middleware.js');
const { Meta } = require('vitreum/headtags'); const { Meta } = require('vitreum/headtags');
@@ -27,6 +28,8 @@ const Markdown = require('naturalcrit/markdown.js');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
const googleDriveIcon = require('../../googleDrive.svg'); const googleDriveIcon = require('../../googleDrive.svg');
const SAVE_TIMEOUT = 3000; const SAVE_TIMEOUT = 3000;
@@ -41,22 +44,24 @@ const EditPage = createClass({
getInitialState : function() { getInitialState : function() {
return { return {
brew : this.props.brew, brew : this.props.brew,
isSaving : false, isSaving : false,
isPending : false, isPending : false,
alertTrashedGoogleBrew : this.props.brew.trashed, alertTrashedGoogleBrew : this.props.brew.trashed,
alertLoginToTransfer : false, alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false, saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false, confirmGoogleTransfer : false,
error : null, error : null,
htmlErrors : Markdown.validate(this.props.brew.text), htmlErrors : Markdown.validate(this.props.brew.text),
url : '', url : '',
autoSave : true, autoSave : true,
autoSaveWarning : false, autoSaveWarning : false,
unsavedTime : new Date(), unsavedTime : new Date(),
currentEditorPage : 0, currentEditorViewPageNum : 1,
displayLockMessage : this.props.brew.lock || false, currentEditorCursorPageNum : 1,
themeBundle : {} currentBrewRendererPageNum : 1,
displayLockMessage : this.props.brew.lock || false,
themeBundle : {}
}; };
}, },
@@ -113,16 +118,27 @@ const EditPage = createClass({
this.editor.current.update(); this.editor.current.update();
}, },
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){ handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback //If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, text: text }, brew : { ...prevState.brew, text: text },
isPending : true, isPending : true,
htmlErrors : htmlErrors, htmlErrors : htmlErrors,
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
}), ()=>{if(this.state.autoSave) this.trySave();}); }), ()=>{if(this.state.autoSave) this.trySave();});
}, },
@@ -150,6 +166,16 @@ const EditPage = createClass({
return !_.isEqual(this.state.brew, this.savedBrew); return !_.isEqual(this.state.brew, this.savedBrew);
}, },
updateBrew : function(newData){
this.setState((prevState)=>({
brew : {
...prevState.brew,
style : newData.style,
text : newData.text
}
}));
},
trySave : function(immediate=false){ trySave : function(immediate=false){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){ if(this.hasChanges()){
@@ -202,6 +228,9 @@ const EditPage = createClass({
htmlErrors : Markdown.validate(prevState.brew.text) htmlErrors : Markdown.validate(prevState.brew.text)
})); }));
updateHistory(this.state.brew);
versionHistoryGarbageCollection();
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
const brew = this.state.brew; const brew = this.state.brew;
@@ -413,6 +442,12 @@ const EditPage = createClass({
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
userThemes={this.props.userThemes} userThemes={this.props.userThemes}
snippetBundle={this.state.themeBundle.snippets} snippetBundle={this.state.themeBundle.snippets}
updateBrew={this.updateBrew}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
@@ -422,7 +457,10 @@ const EditPage = createClass({
themeBundle={this.state.themeBundle} themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors} errors={this.state.htmlErrors}
lang={this.state.brew.lang} lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</SplitPane> </SplitPane>

View File

@@ -161,7 +161,7 @@ const errorIndex = (props)=>{
Please login or signup at our [login page](https://www.naturalcrit.com/login?redirect=https://homebrewery.naturalcrit.com/account).`, Please login or signup at our [login page](https://www.naturalcrit.com/login?redirect=https://homebrewery.naturalcrit.com/account).`,
// Brew locked by Administrators error // Brew locked by Administrators error
'100' : dedent` '51' : dedent`
## This brew has been locked. ## This brew has been locked.
Only an author may request that this lock is removed. Only an author may request that this lock is removed.
@@ -171,6 +171,11 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId} **Brew ID:** ${props.brew.brewId}
**Brew Title:** ${props.brew.brewTitle}`, **Brew Title:** ${props.brew.brewTitle}`,
'90' : dedent` An unexpected error occurred while looking for these brews.
Try again in a few minutes.`,
'91' : dedent` An unexpected error occurred while trying to get the total of brews.`,
}; };
}; };

View File

@@ -1,7 +1,6 @@
require('./homePage.less'); require('./homePage.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require('../../utils/request-middleware.js'); const request = require('../../utils/request-middleware.js');
const { Meta } = require('vitreum/headtags'); const { Meta } = require('vitreum/headtags');
@@ -10,12 +9,12 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx'); const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx');
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx'); const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const { fetchThemeBundle } = require('../../../../shared/helpers.js'); const { fetchThemeBundle } = require('../../../../shared/helpers.js');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
@@ -32,11 +31,13 @@ const HomePage = createClass({
}, },
getInitialState : function() { getInitialState : function() {
return { return {
brew : this.props.brew, brew : this.props.brew,
welcomeText : this.props.brew.text, welcomeText : this.props.brew.text,
error : undefined, error : undefined,
currentEditorPage : 0, currentEditorViewPageNum : 1,
themeBundle : {} currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
themeBundle : {}
}; };
}, },
@@ -61,10 +62,22 @@ const HomePage = createClass({
handleSplitMove : function(){ handleSplitMove : function(){
this.editor.current.update(); this.editor.current.update();
}, },
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){ handleTextChange : function(text){
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, text: text }, brew : { ...prevState.brew, text: text },
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
})); }));
}, },
renderNavbar : function(){ renderNavbar : function(){
@@ -76,6 +89,7 @@ const HomePage = createClass({
} }
<NewBrewItem /> <NewBrewItem />
<HelpNavItem /> <HelpNavItem />
<VaultNavItem />
<RecentNavItem /> <RecentNavItem />
<AccountNavItem /> <AccountNavItem />
</Nav.section> </Nav.section>
@@ -96,12 +110,20 @@ const HomePage = createClass({
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
showEditButtons={false} showEditButtons={false}
snippetBundle={this.state.themeBundle.snippets} snippetBundle={this.state.themeBundle.snippets}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
style={this.state.brew.style} style={this.state.brew.style}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
currentEditorPage={this.state.currentEditorPage} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
themeBundle={this.state.themeBundle} themeBundle={this.state.themeBundle}
/> />
</SplitPane> </SplitPane>

View File

@@ -39,13 +39,15 @@ const NewPage = createClass({
const brew = this.props.brew; const brew = this.props.brew;
return { return {
brew : brew, brew : brew,
isSaving : false, isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false), saveGoogle : (global.account && global.account.googleId ? true : false),
error : null, error : null,
htmlErrors : Markdown.validate(brew.text), htmlErrors : Markdown.validate(brew.text),
currentEditorPage : 0, currentEditorViewPageNum : 1,
themeBundle : {} currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
themeBundle : {}
}; };
}, },
@@ -108,15 +110,26 @@ const NewPage = createClass({
this.editor.current.update(); this.editor.current.update();
}, },
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){ handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback //If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, text: text }, brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors, htmlErrors : htmlErrors,
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
})); }));
localStorage.setItem(BREWKEY, text); localStorage.setItem(BREWKEY, text);
}, },
@@ -221,6 +234,11 @@ const NewPage = createClass({
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
userThemes={this.props.userThemes} userThemes={this.props.userThemes}
snippetBundle={this.state.themeBundle.snippets} snippetBundle={this.state.themeBundle.snippets}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={this.state.brew.text} text={this.state.brew.text}
@@ -230,7 +248,10 @@ const NewPage = createClass({
themeBundle={this.state.themeBundle} themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors} errors={this.state.htmlErrors}
lang={this.state.brew.lang} lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</SplitPane> </SplitPane>

View File

@@ -12,6 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx'); const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const VaultNavitem = require('../../navbar/vault.navitem.jsx');
const UserPage = createClass({ const UserPage = createClass({
displayName : 'UserPage', displayName : 'UserPage',
@@ -66,6 +67,7 @@ const UserPage = createClass({
} }
<NewBrew /> <NewBrew />
<HelpNavItem /> <HelpNavItem />
<VaultNavitem/>
<RecentNavItem /> <RecentNavItem />
<Account /> <Account />
</Nav.section> </Nav.section>

View File

@@ -0,0 +1,396 @@
require('./vaultPage.less');
const React = require('react');
const { useState, useEffect, useRef } = React;
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
const request = require('../../utils/request-middleware.js');
const VaultPage = (props)=>{
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
//Response state
const [brewCollection, setBrewCollection] = useState(null);
const [totalBrews, setTotalBrews] = useState(null);
const [searching, setSearching] = useState(false);
const [error, setError] = useState(null);
const titleRef = useRef(null);
const authorRef = useRef(null);
const countRef = useRef(null);
const v3Ref = useRef(null);
const legacyRef = useRef(null);
const submitButtonRef = useRef(null);
useEffect(()=>{
disableSubmitIfFormInvalid();
loadPage(pageState, true);
}, []);
const updateStateWithBrews = (brews, page)=>{
setBrewCollection(brews || null);
setPageState(parseInt(page) || 1);
setSearching(false);
};
const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page)=>{
const url = new URL(window.location.href);
const urlParams = new URLSearchParams(url.search);
urlParams.set('title', titleValue);
urlParams.set('author', authorValue);
urlParams.set('count', countValue);
urlParams.set('v3', v3Value);
urlParams.set('legacy', legacyValue);
urlParams.set('page', page);
url.search = urlParams.toString();
window.history.replaceState(null, '', url.toString());
};
const performSearch = async (title, author, count, v3, legacy, page)=>{
updateUrl(title, author, count, v3, legacy, page);
const response = await request.get(
`/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}`
).catch((error)=>{
console.log('error at loadPage: ', error);
setError(error);
updateStateWithBrews([], 1);
});
if(response.ok)
updateStateWithBrews(response.body.brews, page);
};
const loadTotal = async (title, author, v3, legacy)=>{
setTotalBrews(null);
const response = await request.get(
`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}`
).catch((error)=>{
console.log('error at loadTotal: ', error);
setError(error);
updateStateWithBrews([], 1);
});
if(response.ok)
setTotalBrews(response.body.totalBrews);
};
const loadPage = async (page, updateTotal)=>{
if(!validateForm())
return;
setSearching(true);
setError(null);
const title = titleRef.current.value || '';
const author = authorRef.current.value || '';
const count = countRef.current.value || 10;
const v3 = v3Ref.current.checked != false;
const legacy = legacyRef.current.checked != false;
performSearch(title, author, count, v3, legacy, page);
if(updateTotal)
loadTotal(title, author, v3, legacy);
};
const renderNavItems = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>
Vault: Search for brews
</Nav.item>
</Nav.section>
<Nav.section>
<NewBrew />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>
);
const validateForm = ()=>{
//form validity: title or author must be written, and at least one renderer set
const isTitleValid = titleRef.current.validity.valid && titleRef.current.value;
const isAuthorValid = authorRef.current.validity.valid && authorRef.current.value;
const isCheckboxChecked = legacyRef.current.checked || v3Ref.current.checked;
const isFormValid = (isTitleValid || isAuthorValid) && isCheckboxChecked;
return isFormValid;
};
const disableSubmitIfFormInvalid = ()=>{
submitButtonRef.current.disabled = !validateForm();
};
const renderForm = ()=>(
<div className='brewLookup'>
<h2 className='formTitle'>Brew Lookup</h2>
<div className='formContents'>
<label>
Title of the brew
<input
ref={titleRef}
type='text'
name='title'
defaultValue={props.query.title || ''}
onKeyUp={disableSubmitIfFormInvalid}
pattern='.{3,}'
title='At least 3 characters'
onKeyDown={(e)=>{
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
loadPage(1, true);
}}
placeholder='v3 Reference Document'
/>
</label>
<label>
Author of the brew
<input
ref={authorRef}
type='text'
name='author'
pattern='.{1,}'
defaultValue={props.query.author || ''}
onKeyUp={disableSubmitIfFormInvalid}
onKeyDown={(e)=>{
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
loadPage(1, true);
}}
placeholder='Username'
/>
</label>
<label>
Results per page
<select ref={countRef} name='count' defaultValue={props.query.count || 20}>
<option value='10'>10</option>
<option value='20'>20</option>
<option value='40'>40</option>
<option value='60'>60</option>
</select>
</label>
<label>
<input
className='renderer'
ref={v3Ref}
type='checkbox'
defaultChecked={props.query.v3 !== 'false'}
onChange={disableSubmitIfFormInvalid}
/>
Search for v3 brews
</label>
<label>
<input
className='renderer'
ref={legacyRef}
type='checkbox'
defaultChecked={props.query.legacy !== 'false'}
onChange={disableSubmitIfFormInvalid}
/>
Search for legacy brews
</label>
<button
id='searchButton'
ref={submitButtonRef}
onClick={()=>{
loadPage(1, true);
}}
>
Search
<i
className={searching ? 'fas fa-spin fa-spinner': 'fas fa-search'}
/>
</button>
</div>
<legend>
<h3>Tips and tricks</h3>
<ul>
<li>
Only <b>published</b> brews are searchable via this tool
</li>
<li>
Usernames are case-sensitive
</li>
<li>
Use <code>"word"</code> to match an exact string,
and <code>-</code> to exclude words (at least one word must not be negated)
</li>
<li>
Some common words like "a", "after", "through", "itself", "here", etc.,
are ignored in searches. The full list can be found &nbsp;
<a href='https://github.com/mongodb/mongo/blob/0e3b3ca8480ddddf5d0105d11a94bd4698335312/src/mongo/db/fts/stop_words_english.txt'>
here
</a>
</li>
</ul>
<small>New features will be coming, such as filters and search by tags.</small>
</legend>
</div>
);
const renderPaginationControls = ()=>{
if(!totalBrews) return null;
const countInt = parseInt(props.query.count || 20);
const totalPages = Math.ceil(totalBrews / countInt);
let startPage, endPage;
if(pageState <= 6) {
startPage = 1;
endPage = Math.min(totalPages, 10);
} else if(pageState + 4 >= totalPages) {
startPage = Math.max(1, totalPages - 9);
endPage = totalPages;
} else {
startPage = pageState - 5;
endPage = pageState + 4;
}
const pagesAroundCurrent = new Array(endPage - startPage + 1)
.fill()
.map((_, index)=>(
<a
key={startPage + index}
className={`pageNumber ${
pageState === startPage + index ? 'currentPage' : ''
}`}
onClick={()=>loadPage(startPage + index, false)}
>
{startPage + index}
</a>
));
return (
<div className='paginationControls'>
<button
className='previousPage'
onClick={()=>loadPage(pageState - 1, false)}
disabled={pageState === startPage}
>
<i className='fa-solid fa-chevron-left'></i>
</button>
<ol className='pages'>
{startPage > 1 && (
<a
className='pageNumber firstPage'
onClick={()=>loadPage(1, false)}
>
1 ...
</a>
)}
{pagesAroundCurrent}
{endPage < totalPages && (
<a
className='pageNumber lastPage'
onClick={()=>loadPage(totalPages, false)}
>
... {totalPages}
</a>
)}
</ol>
<button
className='nextPage'
onClick={()=>loadPage(pageState + 1, false)}
disabled={pageState === totalPages}
>
<i className='fa-solid fa-chevron-right'></i>
</button>
</div>
);
};
const renderFoundBrews = ()=>{
if(searching) {
return (
<div className='foundBrews searching'>
<h3 className='searchAnim'>Searching</h3>
</div>
);
}
if(error) {
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
return (
<div className='foundBrews noBrews'>
<h3>Error: {errorText}</h3>
</div>
);
}
if(!brewCollection) {
return (
<div className='foundBrews noBrews'>
<h3>No search yet</h3>
</div>
);
}
if(brewCollection.length === 0) {
return (
<div className='foundBrews noBrews'>
<h3>No brews found</h3>
</div>
);
}
return (
<div className='foundBrews'>
<span className='totalBrews'>
{`Brews found: `}
<span>{totalBrews}</span>
</span>
{brewCollection.map((brew, index)=>{
return (
<BrewItem
brew={{ ...brew }}
key={index}
reportError={props.reportError}
renderStorage={false}
/>
);
})}
{renderPaginationControls()}
</div>
);
};
return (
<div className='vaultPage'>
<link href='/themes/V3/Blank/style.css' rel='stylesheet' />
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
{renderNavItems()}
<div className='content'>
<SplitPane showDividerButtons={false}>
<div className='form dataGroup'>{renderForm()}</div>
<div className='resultsContainer dataGroup'>
{renderFoundBrews()}
</div>
</SplitPane>
</div>
</div>
);
};
module.exports = VaultPage;

View File

@@ -0,0 +1,362 @@
.vaultPage {
height : 100%;
overflow-y : hidden;
background-color : #2C3E50;
*:not(input) { user-select : none; }
.content {
background : #2C3E50;
height: 100%;
.dataGroup {
width : 100%;
height : 100%;
background : white;
&.form .brewLookup {
position : relative;
padding : 50px clamp(20px, 4vw, 50px);
small {
font-size : 10pt;
color : #555555;
a { color : #333333; }
}
code {
padding-inline : 5px;
background : lightgrey;
border-radius : 5px;
font-family : monospace;
}
h1, h2, h3, h4 {
font-family : 'CodeBold';
letter-spacing : 2px;
}
legend {
h3 {
margin-block : 30px 20px;
font-size : 20px;
text-align : center;
border-bottom : 2px solid;
}
ul {
padding-inline : 30px 10px;
li {
margin-block : 5px;
line-height : calc(1em + 5px);
list-style : disc;
}
}
}
&::after {
position : absolute;
top : 0;
right : 0;
left : 0;
display : block;
padding : 10px;
font-weight : 900;
color : white;
white-space : pre-wrap;
content : 'Error:\A At least one renderer should be enabled to make a search';
background : rgb(255, 60, 60);
opacity : 0;
transition : opacity 0.5s;
}
&:not(:has(input[type='checkbox']:checked))::after { opacity : 1; }
.formTitle {
margin : 20px 0;
font-size : 30px;
color : black;
text-align : center;
border-bottom : 2px solid;
}
.formContents {
position : relative;
display : flex;
flex-direction : column;
label {
display : flex;
align-items : center;
margin : 10px 0;
}
select { margin : 0 10px; }
input {
margin : 0 10px;
&:invalid { background : rgb(255, 188, 181); }
&[type='checkbox'] {
position : relative;
display : inline-block;
width : 50px;
height : 30px;
font-family : 'WalterTurncoat';
font-size : 20px;
font-weight : 800;
color : white;
letter-spacing : 2px;
appearance : none;
background : red;
isolation : isolate;
border-radius : 5px;
&::before,&::after {
position : absolute;
inset : 0;
z-index : 5;
padding-top : 2px;
text-align : center;
}
&::before {
display : block;
content : 'No';
}
&::after {
display : none;
content : 'Yes';
}
&:checked {
background : green;
&::before { display : none; }
&::after { display : block; }
}
}
}
#searchButton {
position : absolute;
right : 20px;
bottom : 0;
i {
margin-left : 10px;
animation-duration : 1000s;
}
}
}
}
&.resultsContainer {
display : flex;
flex-direction : column;
height : 100%;
overflow-y : auto;
font-family : 'BookInsanityRemake';
font-size : 0.34cm;
h3 {
font-family : 'Open Sans';
font-weight : 900;
color : white;
}
.foundBrews {
position : relative;
width : 100%;
height : 100%;
max-height : 100%;
padding : 50px 50px 70px 50px;
overflow-y : scroll;
background-color : #2C3E50;
h3 { font-size : 25px; }
&.noBrews {
display : grid;
place-items : center;
color : white;
}
&.searching {
display : grid;
place-items : center;
color : white;
h3 { position : relative; }
h3.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : calc(100% + 5px) -50%;
animation : trailingDots 2s ease infinite;
}
}
.totalBrews {
position : fixed;
right : 0;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
font-family : 'Open Sans';
font-size : 11px;
font-weight : 800;
color : white;
background-color : #333333;
.searchAnim {
position : relative;
display : inline-block;
width : 3ch;
height : 1em;
}
.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : -50% -50%;
animation : trailingDots 2s ease infinite;
}
}
.brewItem {
width : 47%;
margin-right : 40px;
color : black;
isolation:isolate;
&:after {
position:absolute;
inset:0;
display:block;
content:'';
background-image : url('/assets/parchmentBackground.jpg');
z-index:-1;
}
&:nth-child(even of .brewItem) { margin-right : 0; }
h2 {
font-family : 'MrEavesRemake';
font-size : 0.75cm;
font-weight : 800;
line-height : 0.988em;
color : var(--HB_Color_HeaderText);
}
.info {
font-family : 'ScalySansRemake';
font-size : 1.2em;
position:relative;
z-index:2;
>span {
margin-right : 12px;
line-height : 1.5em;
}
}
.links {
z-index:2;
}
hr {
margin: 0px;
visibility: hidden;
}
.thumbnail {
z-index:1;
}
}
.paginationControls {
position : absolute;
left : 50%;
display : grid;
grid-template-areas : 'previousPage currentPage nextPage';
grid-template-columns : 50px 1fr 50px;
place-items : center;
width : auto;
translate : -50%;
.pages {
display : flex;
grid-area : currentPage;
justify-content : space-evenly;
width : 100%;
height : 100%;
padding : 5px 8px;
text-align : center;
.pageNumber {
margin-inline : 1vw;
font-family : 'Open Sans';
font-weight : 900;
color : white;
text-underline-position : under;
text-wrap : nowrap;
cursor : pointer;
&.currentPage {
color : gold;
text-decoration : underline;
pointer-events : none;
}
&.firstPage { margin-right : -5px; }
&.lastPage { margin-left : -5px; }
}
}
button {
width : max-content;
&.previousPage { grid-area : previousPage; }
&.nextPage { grid-area : nextPage; }
}
}
}
}
}
}
}
@keyframes trailingDots {
0%,
32% { content : ' .'; }
33%,
65% { content : ' ..'; }
66%,
100% { content : ' ...'; }
}
// media query for when the page is smaller than 1079 px in width
@media screen and (max-width : 1079px) {
.vaultPage .content {
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
.dataGroup.resultsContainer .foundBrews .brewItem {
width : 100%;
margin-inline : auto;
}
}
}

View File

@@ -0,0 +1,116 @@
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
export const HISTORY_SLOTS = 5;
// History values in minutes
const DEFAULT_HISTORY_SAVE_DELAYS = {
'0' : 0,
'1' : 2,
'2' : 10,
'3' : 60,
'4' : 12 * 60,
'5' : 2 * 24 * 60
};
const DEFAULT_GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
const HISTORY_SAVE_DELAYS = global.config?.historyData?.HISTORY_SAVE_DELAYS ?? DEFAULT_HISTORY_SAVE_DELAYS;
const GARBAGE_COLLECT_DELAY = global.config?.historyData?.GARBAGE_COLLECT_DELAY ?? DEFAULT_GARBAGE_COLLECT_DELAY;
function getKeyBySlot(brew, slot){
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
};
function getVersionBySlot(brew, slot){
// Read stored brew data
// - If it exists, parse data to object
// - If it doesn't exist, pass default object
const key = getKeyBySlot(brew, slot);
const storedVersion = localStorage.getItem(key);
const output = storedVersion ? JSON.parse(storedVersion) : { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
return output;
};
function updateStoredBrew(brew, slot = 0) {
const archiveBrew = {
title : brew.title,
text : brew.text,
style : brew.style,
version : brew.version,
shareId : brew.shareId,
savedAt : brew?.savedAt || new Date(),
expireAt : new Date()
};
archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]);
const key = getKeyBySlot(brew, slot);
localStorage.setItem(key, JSON.stringify(archiveBrew));
}
export function historyExists(brew){
return Object.keys(localStorage)
.some((key)=>{
return key.startsWith(`${HISTORY_PREFIX}-${brew.shareId}`);
});
}
export function loadHistory(brew){
const history = {};
// Load data from local storage to History object
for (let i = 1; i <= HISTORY_SLOTS; i++){
history[i] = getVersionBySlot(brew, i);
};
return history;
}
export function updateHistory(brew) {
const history = loadHistory(brew);
// Walk each version position
for (let slot = HISTORY_SLOTS; slot > 0; slot--){
const storedVersion = history[slot];
// If slot has expired, update all lower slots and break
if(new Date() >= new Date(storedVersion.expireAt)){
for (let updateSlot = slot - 1; updateSlot>0; updateSlot--){
// Move data from updateSlot to updateSlot + 1
!history[updateSlot]?.noData && updateStoredBrew(history[updateSlot], updateSlot + 1);
};
// Update the most recent brew
updateStoredBrew(brew, 1);
// Break out of data checks because we found an expired value
break;
}
};
};
export function getHistoryItems(brew){
const historyArray = [];
for (let i = 1; i <= HISTORY_SLOTS; i++){
historyArray.push(getVersionBySlot(brew, i));
}
return historyArray;
};
export function versionHistoryGarbageCollection(){
Object.keys(localStorage)
.filter((key)=>{
return key.startsWith(HISTORY_PREFIX);
})
.forEach((key)=>{
const collectAt = new Date(JSON.parse(localStorage.getItem(key)).savedAt);
collectAt.setMinutes(collectAt.getMinutes() + GARBAGE_COLLECT_DELAY);
if(new Date() > collectAt){
localStorage.removeItem(key);
}
});
};

232
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"version": "3.14.2", "version": "3.15.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "homebrewery", "name": "homebrewery",
"version": "3.14.2", "version": "3.15.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -14,7 +14,7 @@
"@babel/plugin-transform-runtime": "^7.25.4", "@babel/plugin-transform-runtime": "^7.25.4",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.13.1", "@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
@@ -23,9 +23,9 @@
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.19.2", "express": "^4.21.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.8",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
@@ -38,22 +38,22 @@
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.6.0", "mongoose": "^8.6.2",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.26.1", "react-router-dom": "6.26.2",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.1.0", "superagent": "^10.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.0.1", "@stylistic/stylelint-plugin": "^3.0.1",
"eslint": "^9.9.1", "eslint": "^9.10.0",
"eslint-plugin-jest": "^28.8.0", "eslint-plugin-jest": "^28.8.3",
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.36.1",
"globals": "^15.9.0", "globals": "^15.9.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
@@ -2073,9 +2073,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.9.1", "version": "9.10.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
"integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==", "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2090,10 +2090,22 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/plugin-kit": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
"integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
"dev": true,
"dependencies": {
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@googleapis/drive": { "node_modules/@googleapis/drive": {
"version": "8.13.1", "version": "8.14.0",
"resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.13.1.tgz", "resolved": "https://registry.npmjs.org/@googleapis/drive/-/drive-8.14.0.tgz",
"integrity": "sha512-ODfl4VUIKNox570DFA6AzAEHQcKI1EQs0xzzupeAIa+S/kFan85TItXU7XywK8mDORnfkgqwXQ5N/u/BFBj5lw==", "integrity": "sha512-AOokfpP6pCdcJXWA8khaCEgbGpWYavWTdAAhL4idbbf2VCQcJ2f7vPalAYNu6a4Sfj0Ly4Ehnd1xw9J9TixB1A==",
"dependencies": { "dependencies": {
"googleapis-common": "^7.0.0" "googleapis-common": "^7.0.0"
}, },
@@ -3013,9 +3025,9 @@
} }
}, },
"node_modules/@remix-run/router": { "node_modules/@remix-run/router": {
"version": "1.19.1", "version": "1.19.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
"integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==", "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
} }
@@ -4128,10 +4140,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.2", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
@@ -4141,7 +4152,7 @@
"http-errors": "2.0.0", "http-errors": "2.0.0",
"iconv-lite": "0.4.24", "iconv-lite": "0.4.24",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"qs": "6.11.0", "qs": "6.13.0",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "1.0.0"
@@ -5589,10 +5600,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@@ -5819,16 +5829,17 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.9.1", "version": "9.10.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
"integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==", "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0", "@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0", "@eslint/config-array": "^0.18.0",
"@eslint/eslintrc": "^3.1.0", "@eslint/eslintrc": "^3.1.0",
"@eslint/js": "9.9.1", "@eslint/js": "9.10.0",
"@eslint/plugin-kit": "^0.1.0",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0", "@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
@@ -5851,7 +5862,6 @@
"is-glob": "^4.0.0", "is-glob": "^4.0.0",
"is-path-inside": "^3.0.3", "is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1", "json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -5878,9 +5888,9 @@
} }
}, },
"node_modules/eslint-plugin-jest": { "node_modules/eslint-plugin-jest": {
"version": "28.8.0", "version": "28.8.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz",
"integrity": "sha512-Tubj1hooFxCl52G4qQu0edzV/+EZzPUeN8p2NnW5uu4fbDs+Yo7+qDVDc4/oG3FbCqEBmu/OC3LSsyiU22oghw==", "integrity": "sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -5903,11 +5913,10 @@
} }
}, },
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.35.0", "version": "7.36.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
"integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"array-includes": "^3.1.8", "array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5", "array.prototype.findlast": "^1.2.5",
@@ -6306,37 +6315,36 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.19.2", "version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.2", "body-parser": "1.20.3",
"content-disposition": "0.5.4", "content-disposition": "0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.6.0", "cookie": "0.6.0",
"cookie-signature": "1.0.6", "cookie-signature": "1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.2.0", "finalhandler": "1.3.1",
"fresh": "0.5.2", "fresh": "0.5.2",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"merge-descriptors": "1.0.1", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7", "path-to-regexp": "0.1.10",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.11.0", "qs": "6.13.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.18.0", "send": "0.19.0",
"serve-static": "1.15.0", "serve-static": "1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
@@ -6354,12 +6362,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/express-static-gzip": { "node_modules/express-static-gzip": {
"version": "2.1.7", "version": "2.1.8",
"resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.7.tgz", "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.8.tgz",
"integrity": "sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw==", "integrity": "sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"serve-static": "^1.14.1" "serve-static": "^1.16.2"
} }
}, },
"node_modules/express/node_modules/cookie": { "node_modules/express/node_modules/cookie": {
@@ -6575,13 +6582,12 @@
} }
}, },
"node_modules/finalhandler": { "node_modules/finalhandler": {
"version": "1.2.0", "version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"on-finished": "2.4.1", "on-finished": "2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
@@ -6596,7 +6602,6 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -6604,8 +6609,7 @@
"node_modules/finalhandler/node_modules/ms": { "node_modules/finalhandler/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
"license": "MIT"
}, },
"node_modules/find-up": { "node_modules/find-up": {
"version": "5.0.0", "version": "5.0.0",
@@ -10524,10 +10528,12 @@
} }
}, },
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "1.0.1", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT" "funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}, },
"node_modules/merge-stream": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
@@ -10795,9 +10801,9 @@
} }
}, },
"node_modules/mongoose": { "node_modules/mongoose": {
"version": "8.6.0", "version": "8.6.2",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.0.tgz", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.2.tgz",
"integrity": "sha512-p6VSbYKvD4ZIabqo8C0kS5eKX1Xpji+opTAIJ9wyuPJ8Y/FblgXSMnFRXnB40bYZLKPQT089K5KU8+bqIXtFdw==", "integrity": "sha512-ErbDVvuUzUfyQpXvJ6sXznmZDICD8r6wIsa0VKjJtB6/LZncqwUn5Um040G1BaNo6L3Jz+xItLSwT0wZmSmUaQ==",
"dependencies": { "dependencies": {
"bson": "^6.7.0", "bson": "^6.7.0",
"kareem": "2.6.3", "kareem": "2.6.3",
@@ -11638,10 +11644,9 @@
} }
}, },
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
"license": "MIT"
}, },
"node_modules/path-type": { "node_modules/path-type": {
"version": "4.0.0", "version": "4.0.0",
@@ -12075,12 +12080,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.4" "side-channel": "^1.0.6"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -12205,11 +12209,11 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.26.1", "version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
"integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.19.1" "@remix-run/router": "1.19.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -12219,12 +12223,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.26.1", "version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
"integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
"dependencies": { "dependencies": {
"@remix-run/router": "1.19.1", "@remix-run/router": "1.19.2",
"react-router": "6.26.1" "react-router": "6.26.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -12722,10 +12726,9 @@
} }
}, },
"node_modules/send": { "node_modules/send": {
"version": "0.18.0", "version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"license": "MIT",
"dependencies": { "dependencies": {
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
@@ -12749,7 +12752,6 @@
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"dependencies": { "dependencies": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -12757,25 +12759,30 @@
"node_modules/send/node_modules/debug/node_modules/ms": { "node_modules/send/node_modules/debug/node_modules/ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
"license": "MIT" },
"node_modules/send/node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
"engines": {
"node": ">= 0.8"
}
}, },
"node_modules/send/node_modules/ms": { "node_modules/send/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
"license": "MIT"
}, },
"node_modules/serve-static": { "node_modules/serve-static": {
"version": "1.15.0", "version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"license": "MIT",
"dependencies": { "dependencies": {
"encodeurl": "~1.0.2", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"send": "0.18.0" "send": "0.19.0"
}, },
"engines": { "engines": {
"node": ">= 0.8.0" "node": ">= 0.8.0"
@@ -14617,21 +14624,6 @@
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/url/node_modules/qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/use": { "node_modules/use": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.14.2", "version": "3.15.0",
"engines": { "engines": {
"npm": "^10.2.x", "npm": "^10.2.x",
"node": "^20.8.x" "node": "^20.8.x"
@@ -89,7 +89,7 @@
"@babel/plugin-transform-runtime": "^7.25.4", "@babel/plugin-transform-runtime": "^7.25.4",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.13.1", "@googleapis/drive": "^8.14.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
@@ -98,9 +98,9 @@
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.19.2", "express": "^4.21.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.8",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
@@ -113,22 +113,22 @@
"marked-smartypants-lite": "^1.0.2", "marked-smartypants-lite": "^1.0.2",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.6.0", "mongoose": "^8.6.2",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-frame-component": "^4.1.3", "react-frame-component": "^4.1.3",
"react-router-dom": "6.26.1", "react-router-dom": "6.26.2",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.1.0", "superagent": "^10.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.0.1", "@stylistic/stylelint-plugin": "^3.0.1",
"eslint": "^9.9.1", "eslint": "^9.10.0",
"eslint-plugin-jest": "^28.8.0", "eslint-plugin-jest": "^28.8.3",
"eslint-plugin-react": "^7.35.0", "eslint-plugin-react": "^7.36.1",
"globals": "^15.9.0", "globals": "^15.9.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",

File diff suppressed because it is too large Load Diff

View File

@@ -99,7 +99,7 @@ const api = {
stub = stub?.toObject(); stub = stub?.toObject();
if(stub?.lock?.locked && accessType != 'edit') { if(stub?.lock?.locked && accessType != 'edit') {
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
} }
// If there is a google id, try to find the google brew // If there is a google id, try to find the google brew

View File

@@ -309,7 +309,7 @@ describe('Tests for api', ()=>{
const req = { brew: {} }; const req = { brew: {} };
const next = jest.fn(); const next = jest.fn();
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' }); await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
}); });
}); });
@@ -934,7 +934,7 @@ brew`);
expect(req.brew).toEqual(testBrew); expect(req.brew).toEqual(testBrew);
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n'); expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
expect(res.status).toHaveBeenCalledWith(200); expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith("\nI Have a style!\n"); expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
expect(res.set).toHaveBeenCalledWith({ expect(res.set).toHaveBeenCalledWith({
'Cache-Control' : 'no-cache', 'Cache-Control' : 'no-cache',
'Content-Type' : 'text/css' 'Content-Type' : 'text/css'

102
server/vault.api.js Normal file
View File

@@ -0,0 +1,102 @@
const express = require('express');
const asyncHandler = require('express-async-handler');
const HomebrewModel = require('./homebrew.model.js').model;
const router = express.Router();
const titleConditions = (title)=>{
if(!title) return {};
return {
$text : {
$search : title,
$caseSensitive : false,
},
};
};
const authorConditions = (author)=>{
if(!author) return {};
return { authors: author };
};
const rendererConditions = (legacy, v3)=>{
if(legacy === 'true' && v3 !== 'true')
return { renderer: 'legacy' };
if(v3 === 'true' && legacy !== 'true')
return { renderer: 'V3' };
return {}; // If all renderers selected, renderer field not needed in query for speed
};
const findBrews = async (req, res)=>{
const title = req.query.title || '';
const author = req.query.author || '';
const page = Math.max(parseInt(req.query.page) || 1, 1);
const count = Math.max(parseInt(req.query.count) || 20, 10);
const skip = (page - 1) * count;
const combinedQuery = {
$and : [
{ published: true },
rendererConditions(req.query.legacy, req.query.v3),
titleConditions(title),
authorConditions(author)
],
};
const projection = {
editId : 0,
googleId : 0,
text : 0,
textBin : 0,
version : 0
};
await HomebrewModel.find(combinedQuery, projection)
.skip(skip)
.limit(count)
.maxTimeMS(5000)
.exec()
.then((brews)=>{
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const processedBrews = brews.map((brew)=>{
brew.authors = brew.authors.map((author)=>emailRegex.test(author) ? 'hidden' : author
);
return brew;
});
res.json({ brews: processedBrews, page });
})
.catch((error)=>{
throw { ...error, message: 'Error finding brews in Vault search', HBErrorCode: 90 };
});
};
const findTotal = async (req, res)=>{
const title = req.query.title || '';
const author = req.query.author || '';
const combinedQuery = {
$and : [
{ published: true },
rendererConditions(req.query.legacy, req.query.v3),
titleConditions(title),
authorConditions(author)
],
};
await HomebrewModel.countDocuments(combinedQuery)
.then((totalBrews)=>{
console.log(`when returning, the total of brews is ${totalBrews} for the query ${JSON.stringify(combinedQuery)}`);
res.json({ totalBrews });
})
.catch((error)=>{
throw { ...error, message: 'Error finding brews in Vault search findTotal function', HBErrorCode: 91 };
});
};
router.get('/api/vault/total', asyncHandler(findTotal));
router.get('/api/vault', asyncHandler(findBrews));
module.exports = router;

View File

@@ -397,6 +397,11 @@ const CodeEditor = createClass({
getCursorPosition : function(){ getCursorPosition : function(){
return this.codeMirror.getCursor(); return this.codeMirror.getCursor();
}, },
getTopVisibleLine : function(){
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
return topVisibleLine;
},
updateSize : function(){ updateSize : function(){
this.codeMirror.refresh(); this.codeMirror.refresh();
}, },

View File

@@ -105,16 +105,16 @@ renderer.link = function (href, title, text) {
// Expose `src` attribute as `--HB_src` to make the URL accessible via CSS // Expose `src` attribute as `--HB_src` to make the URL accessible via CSS
renderer.image = function (href, title, text) { renderer.image = function (href, title, text) {
href = cleanUrl(href); href = cleanUrl(href);
if (href === null) if(href === null)
return text; return text;
let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`; let out = `<img src="${href}" alt="${text}" style="--HB_src:url(${href});"`;
if (title) if(title)
out += ` title="${title}"`; out += ` title="${title}"`;
out += '>'; out += '>';
return out; return out;
} };
// Disable default reflink behavior, as it steps on our variables extension // Disable default reflink behavior, as it steps on our variables extension
tokenizer.def = function () { tokenizer.def = function () {
@@ -745,7 +745,7 @@ const tableTerminators = [
`:+\\n`, // hardBreak `:+\\n`, // hardBreak
` *{[^\n]+}`, // blockInjector ` *{[^\n]+}`, // blockInjector
` *{{[^{\n]*\n.*?\n}}` // mustacheDiv ` *{{[^{\n]*\n.*?\n}}` // mustacheDiv
] ];
Marked.use(MarkedVariables()); Marked.use(MarkedVariables());
Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts, Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts,
@@ -755,12 +755,12 @@ Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false });
Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions));
function cleanUrl(href) { function cleanUrl(href) {
try { try {
href = encodeURI(href).replace(/%25/g, '%'); href = encodeURI(href).replace(/%25/g, '%');
} catch { } catch {
return null; return null;
} }
return href; return href;
} }
const escapeTest = /[&<>"']/; const escapeTest = /[&<>"']/;

View File

@@ -7,8 +7,9 @@ const SplitPane = createClass({
displayName : 'SplitPane', displayName : 'SplitPane',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
storageKey : 'naturalcrit-pane-split', storageKey : 'naturalcrit-pane-split',
onDragFinish : function(){} //fires when dragging onDragFinish : function(){}, //fires when dragging
showDividerButtons : true
}; };
}, },
@@ -41,6 +42,10 @@ const SplitPane = createClass({
}); });
} }
window.addEventListener('resize', this.handleWindowResize); window.addEventListener('resize', this.handleWindowResize);
// This lives here instead of in the initial render because you cannot touch localStorage until the componant mounts.
const loadLiveScroll = window.localStorage.getItem('liveScroll') === 'true';
this.setState({ liveScroll: loadLiveScroll });
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
@@ -88,6 +93,11 @@ const SplitPane = createClass({
userSetDividerPos : newSize userSetDividerPos : newSize
}); });
}, },
liveScrollToggle : function() {
window.localStorage.setItem('liveScroll', String(!this.state.liveScroll));
this.setState({ liveScroll: !this.state.liveScroll });
},
/* /*
unFocus : function() { unFocus : function() {
if(document.selection){ if(document.selection){
@@ -119,13 +129,18 @@ const SplitPane = createClass({
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} > onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
<i className='fas fa-arrow-right' /> <i className='fas fa-arrow-right' />
</div> </div>
<div id='scrollToggleDiv' className={this.state.liveScroll ? 'arrow lock' : 'arrow unlock'}
style={{ left: this.state.currentDividerPos-4 }}
onClick={this.liveScrollToggle} >
<i id='scrollToggle' className={this.state.liveScroll ? 'fas fa-lock' : 'fas fa-unlock'} />
</div>
</>; </>;
} }
}, },
renderDivider : function(){ renderDivider : function(){
return <> return <>
{this.renderMoveArrows()} {this.props.showDividerButtons && this.renderMoveArrows()}
<div className='divider' onPointerDown={this.handleDown} > <div className='divider' onPointerDown={this.handleDown} >
<div className='dots'> <div className='dots'>
<i className='fas fa-circle' /> <i className='fas fa-circle' />
@@ -142,9 +157,12 @@ const SplitPane = createClass({
width={this.state.currentDividerPos} width={this.state.currentDividerPos}
> >
{React.cloneElement(this.props.children[0], { {React.cloneElement(this.props.children[0], {
moveBrew : this.state.moveBrew, ...(this.props.showDividerButtons && {
moveSource : this.state.moveSource, moveBrew : this.state.moveBrew,
setMoveArrows : this.setMoveArrows moveSource : this.state.moveSource,
liveScroll : this.state.liveScroll,
setMoveArrows : this.setMoveArrows,
}),
})} })}
</Pane> </Pane>
{this.renderDivider()} {this.renderDivider()}

View File

@@ -53,6 +53,15 @@
.tooltipRight('Jump to location in Preview'); .tooltipRight('Jump to location in Preview');
top : 60px; top : 60px;
} }
&.lock{
.tooltipRight('De-sync Editor and Preview locations.');
top : 90px;
background: #666;
}
&.unlock{
.tooltipRight('Sync Editor and Preview locations');
top : 90px;
}
&:hover{ &:hover{
background-color: #666; background-color: #666;
} }

View File

@@ -27,35 +27,154 @@ module.exports = [
experimental : true, experimental : true,
subsnippets : [ subsnippets : [
{ {
name : 'Table of Contents', name : 'Generate Table of Contents',
icon : 'fas fa-book', icon : 'fas fa-book',
gen : TableOfContentsGen, gen : TableOfContentsGen,
experimental : true experimental : true
}, },
{ {
name : 'Include in ToC up to H3', name : 'Table of Contents Individual Inclusion',
icon : 'fas fa-dice-three', icon : 'fas fa-book',
gen : dedent `\n{{tocInclude# CHANGE # to your header level
}}\n`,
subsnippets : [
{
name : 'Individual Inclusion H1',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH1 \n
}}\n`,
},
{
name : 'Individual Inclusion H2',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH2 \n
}}\n`,
},
{
name : 'Individual Inclusion H3',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH3 \n
}}\n`,
},
{
name : 'Individual Inclusion H4',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH4 \n
}}\n`,
},
{
name : 'Individual Inclusion H5',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH5 \n
}}\n`,
},
{
name : 'Individual Inclusion H6',
icon : 'fas fa-book',
gen : dedent `\n{{tocIncludeH6 \n
}}\n`,
}
]
},
{
name : 'Table of Contents Range Inclusion',
icon : 'fas fa-book',
gen : dedent `\n{{tocDepthH3 gen : dedent `\n{{tocDepthH3
}}\n`, }}\n`,
subsnippets : [
{
name : 'Include in ToC up to H3',
icon : 'fas fa-dice-three',
gen : dedent `\n{{tocDepthH3
}}\n`,
},
{
name : 'Include in ToC up to H4',
icon : 'fas fa-dice-four',
gen : dedent `\n{{tocDepthH4
}}\n`,
},
{
name : 'Include in ToC up to H5',
icon : 'fas fa-dice-five',
gen : dedent `\n{{tocDepthH5
}}\n`,
},
{
name : 'Include in ToC up to H6',
icon : 'fas fa-dice-six',
gen : dedent `\n{{tocDepthH6
}}\n`,
},
]
}, },
{ {
name : 'Include in ToC up to H4', name : 'Table of Contents Individual Exclusion',
icon : 'fas fa-dice-four', icon : 'fas fa-book',
gen : dedent `\n{{tocDepthH4 gen : dedent `\n{{tocExcludeH1 \n
}}\n`, }}\n`,
subsnippets : [
{
name : 'Individual Exclusion H1',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH1 \n
}}\n`,
},
{
name : 'Individual Exclusion H2',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH2 \n
}}\n`,
},
{
name : 'Individual Exclusion H3',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH3 \n
}}\n`,
},
{
name : 'Individual Exclusion H4',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH4 \n
}}\n`,
},
{
name : 'Individual Exclusion H5',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH5 \n
}}\n`,
},
{
name : 'Individual Exclusion H6',
icon : 'fas fa-book',
gen : dedent `\n{{tocExcludeH6 \n
}}\n`,
},
]
}, },
{ {
name : 'Include in ToC up to H5', name : 'Table of Contents Toggles',
icon : 'fas fa-dice-five', icon : 'fas fa-book',
gen : dedent `\n{{tocDepthH5 gen : `{{tocGlobalH4}}\n\n`,
}}\n`, subsnippets : [
}, {
{ name : 'Enable H1-H4 all pages',
name : 'Include in ToC up to H6', icon : 'fas fa-dice-four',
icon : 'fas fa-dice-six', gen : `{{tocGlobalH4}}\n\n`,
gen : dedent `\n{{tocDepthH6 },
}}\n`, {
name : 'Enable H1-H5 all pages',
icon : 'fas fa-dice-five',
gen : `{{tocGlobalH5}}\n\n`,
},
{
name : 'Enable H1-H6 all pages',
icon : 'fas fa-dice-six',
gen : `{{tocGlobalH6}}\n\n`,
},
]
} }
] ]
}, },
@@ -94,7 +213,7 @@ module.exports = [
background-image: linear-gradient(-45deg, #322814, #998250, #322814); background-image: linear-gradient(-45deg, #322814, #998250, #322814);
line-height: 1em; line-height: 1em;
}\n\n` }\n\n`
} },
] ]
}, },

View File

@@ -1,77 +1,78 @@
const _ = require('lodash');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const getTOC = (pages)=>{ // Map each actual page to its footer label, accounting for skips or numbering resets
const mapPages = (pages)=>{
let actualPage = 0;
let mappedPage = 0; // Number displayed in footer
const pageMap = [];
const recursiveAdd = (title, page, targetDepth, child, curDepth=0)=>{ pages.forEach((page)=>{
if(curDepth > 5) return; // Something went wrong. actualPage++;
if(curDepth == targetDepth) { const doSkip = page.querySelector('.skipCounting');
child.push({ const doReset = page.querySelector('.resetCounting');
title : title,
page : page, if(doReset)
children : [] mappedPage = 1;
}); if(!doSkip && !doReset)
} else { mappedPage++;
if(child.length == 0) {
child.push({ pageMap[actualPage] = {
title : null, mappedPage : mappedPage,
page : page, showPage : !doSkip
children : [] };
}); });
return pageMap;
};
const getMarkdown = (headings, pageMap)=>{
const levelPad = ['- ###', ' - ####', ' -', ' -', ' -', ' -'];
const allMarkdown = [];
const depthChain = [0];
headings.forEach((heading)=>{
const page = parseInt(heading.closest('.page').id?.replace(/^p/, ''));
const mappedPage = pageMap[page].mappedPage;
const showPage = pageMap[page].showPage;
const title = heading.textContent.trim();
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
const depth = parseInt(heading.tagName.substring(1));
if(!title || !showPage || ToCExclude == 'exclude')
return;
//If different header depth than last, remove indents until nearest higher-level header, then indent once
if(depth !== depthChain[depthChain.length -1]) {
while (depth <= depthChain[depthChain.length - 1]) {
depthChain.pop();
} }
recursiveAdd(title, page, targetDepth, _.last(child).children, curDepth+1,); depthChain.push(depth);
} }
};
const res = []; const markdown = `${levelPad[depthChain.length - 2]} [{{ ${title}}}{{ ${mappedPage}}}](#p${page})`;
allMarkdown.push(markdown);
});
return allMarkdown.join('\n');
};
const getTOC = ()=>{
const iframe = document.getElementById('BrewRenderer'); const iframe = document.getElementById('BrewRenderer');
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document; const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6'); const headings = iframeDocument.querySelectorAll('h1, h2, h3, h4, h5, h6');
const headerDepth = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']; const pages = iframeDocument.querySelectorAll('.page');
_.each(headings, (heading)=>{ const pageMap = mapPages(pages);
const onPage = parseInt(heading.closest('.page').id?.replace(/^p/, '')); return getMarkdown(headings, pageMap);
const ToCExclude = getComputedStyle(heading).getPropertyValue('--TOC');
if(ToCExclude != 'exclude') {
recursiveAdd(heading.textContent.trim(), onPage, headerDepth.indexOf(heading.tagName), res);
}
});
return res;
};
const ToCIterate = (entries, curDepth=0)=>{
const levelPad = ['- ###', ' - ####', ' - ', ' - ', ' - ', ' - '];
const toc = [];
if(entries.title !== null){
toc.push(`${levelPad[curDepth]} [{{ ${entries.title}}}{{ ${entries.page}}}](#p${entries.page})`);
}
if(entries.children.length) {
_.each(entries.children, (entry, idx)=>{
const children = ToCIterate(entry, entry.title == null ? curDepth : curDepth+1);
if(children.length) {
toc.push(...children);
}
});
}
return toc;
}; };
module.exports = function(props){ module.exports = function(props){
const pages = props.brew.text.split('\\page'); const TOC = getTOC();
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(ToCIterate(g1).join('\n'));
return r;
}, []).join('\n');
return dedent` return dedent`
{{toc,wide {{toc,wide
# Contents # Contents
${markdown} ${TOC}
}} }}
\n`; \n`;
}; };

View File

@@ -11,6 +11,7 @@
--HB_Color_CaptionText : #766649; // Brown --HB_Color_CaptionText : #766649; // Brown
--HB_Color_WatercolorStain : #BBAD82; // Light brown --HB_Color_WatercolorStain : #BBAD82; // Light brown
--HB_Color_Footnotes : #C9AD6A; // Gold --HB_Color_Footnotes : #C9AD6A; // Gold
--TOC : 'include';
} }
.useSansSerif() { .useSansSerif() {
@@ -797,7 +798,7 @@
// *****************************/ // *****************************/
// Default Exclusions // Default Exclusions
// Anything not exlcuded is included, default Headers are H1, H2, and H3. // Anything not excluded is included, default Headers are H1, H2, and H3.
h4, h4,
h5, h5,
h6, h6,
@@ -808,12 +809,23 @@ h6,
.noToC, .noToC,
.toc { --TOC: exclude; } .toc { --TOC: exclude; }
.tocDepthH2 :is(h1, h2) {--TOC: include; }
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
// Brew level default inclusion changes.
// These add Headers 'back' to inclusion.
.pages:has(.tocGlobalH4) {
h4 {--TOC: include; }
}
.pages:has(.tocGlobalH5) {
h4, h5 {--TOC: include; }
}
.pages:has(.tocGlobalH6) {
h4, h5, h6 {--TOC: include; }
}
// Block level inclusion changes
// These include either a single (include) or a range (depth)
.tocIncludeH1 h1 {--TOC: include; } .tocIncludeH1 h1 {--TOC: include; }
.tocIncludeH2 h2 {--TOC: include; } .tocIncludeH2 h2 {--TOC: include; }
.tocIncludeH3 h3 {--TOC: include; } .tocIncludeH3 h3 {--TOC: include; }
@@ -821,6 +833,21 @@ h6,
.tocIncludeH5 h5 {--TOC: include; } .tocIncludeH5 h5 {--TOC: include; }
.tocIncludeH6 h6 {--TOC: include; } .tocIncludeH6 h6 {--TOC: include; }
.tocDepthH2 :is(h1, h2) {--TOC: include; }
.tocDepthH3 :is(h1, h2, h3) {--TOC: include; }
.tocDepthH4 :is(h1, h2, h3, h4) {--TOC: include; }
.tocDepthH5 :is(h1, h2, h3, h4, h5) {--TOC: include; }
.tocDepthH6 :is(h1, h2, h3, h4, h5, h6) {--TOC: include; }
// Block level exclusion changes
// These exclude a single block level
.tocExcludeH1 h1 {--TOC: exclude; }
.tocExcludeH2 h2 {--TOC: exclude; }
.tocExcludeH3 h3 {--TOC: exclude; }
.tocExcludeH4 h4 {--TOC: exclude; }
.tocExcludeH5 h5 {--TOC: exclude; }
.tocExcludeH6 h6 {--TOC: exclude; }
.page:has(.partCover) { .page:has(.partCover) {
--TOC: exclude; --TOC: exclude;
& h1 { & h1 {

View File

@@ -23,14 +23,30 @@ module.exports = [
gen : '\n\\page\n' gen : '\n\\page\n'
}, },
{ {
name : 'Page Number', name : 'Page Numbering',
icon : 'fas fa-bookmark', icon : 'fas fa-bookmark',
gen : '{{pageNumber 1}}\n' subsnippets : [
}, {
{ name : 'Page Number',
name : 'Auto-incrementing Page Number', icon : 'fas fa-bookmark',
icon : 'fas fa-sort-numeric-down', gen : '{{pageNumber 1}}\n'
gen : '{{pageNumber,auto}}\n' },
{
name : 'Auto-incrementing Page Number',
icon : 'fas fa-sort-numeric-down',
gen : '{{pageNumber,auto}}\n'
},
{
name : 'Skip Page Number Increment this Page',
icon : 'fas fa-xmark',
gen : '{{skipCounting}}\n'
},
{
name : 'Restart Numbering',
icon : 'fas fa-arrow-rotate-left',
gen : '{{resetCounting}}\n'
},
]
}, },
{ {
name : 'Footer', name : 'Footer',

View File

@@ -12,7 +12,7 @@
} }
@page { margin : 0; } @page { margin : 0; }
body { counter-reset : page-numbers; } body { counter-reset : page-numbers 0; }
* { -webkit-print-color-adjust : exact; } * { -webkit-print-color-adjust : exact; }
//***************************** //*****************************
@@ -51,7 +51,6 @@ body { counter-reset : page-numbers; }
height : 279.4mm; height : 279.4mm;
padding : 1.4cm 1.9cm 1.7cm; padding : 1.4cm 1.9cm 1.7cm;
overflow : hidden; overflow : hidden;
counter-increment : page-numbers;
background-color : var(--HB_Color_Background); background-color : var(--HB_Color_Background);
text-rendering : optimizeLegibility; text-rendering : optimizeLegibility;
contain : size; contain : size;
@@ -494,4 +493,13 @@ body { counter-reset : page-numbers; }
&:nth-child(even) { &:nth-child(even) {
.pageNumber { left : 30px; } .pageNumber { left : 30px; }
} }
}
.resetCounting {
counter-set : page-numbers 1;
}
&:not(:has(.skipCounting)) {
counter-increment : page-numbers;
}
}