Merge branch 'master' into experimentalMarkedDiagrams

This commit is contained in:
G.Ambatte
2026-03-26 18:47:51 +13:00
163 changed files with 4796 additions and 2350 deletions
+29 -1
View File
@@ -83,12 +83,40 @@ pre {
.page .exampleTable td,th { .page .exampleTable td,th {
border:1px dashed #00000030; border:1px dashed #00000030;
} }
.page .df {
font-size: 2em;
vertical-align: middle;
}
``` ```
## 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).
### Friday 11/14/2025 - v13.20.0 ### Friday 1/11/2026 - v3.20.1
{{taskList
##### calculuschild
* [x] Add D100 "ball" dice icons `:d100:` :df_d100_05:
##### G-Ambatte
* [x] Fix transparent edge on back cover image
Fixes issue [#4551](https://github.com/naturalcrit/homebrewery/issues/4551)
* [x] Fix "Out of sync" error when document contains extended unicode characters
Fixes issue [#4583](https://github.com/naturalcrit/homebrewery/issues/4583)
##### 5e-Cleric
* [x] Fix page count error on Vault
* [x] Fix cover page footnote set to all-caps
Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559)
}}
### Friday 11/14/2025 - v3.20.0
{{taskList {{taskList
##### calculuschild ##### calculuschild
+3 -3
View File
@@ -1,7 +1,7 @@
import './admin.less'; import './admin.less';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
const BrewUtils = require('./brewUtils/brewUtils.jsx'); import BrewUtils from './brewUtils/brewUtils.jsx';
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx'); import NotificationUtils from './notificationUtils/notificationUtils.jsx';
import AuthorUtils from './authorUtils/authorUtils.jsx'; import AuthorUtils from './authorUtils/authorUtils.jsx';
import LockTools from './lockTools/lockTools.jsx'; import LockTools from './lockTools/lockTools.jsx';
@@ -49,4 +49,4 @@ const Admin = ()=>{
); );
}; };
module.exports = Admin; export default Admin;
+6 -8
View File
@@ -1,11 +1,9 @@
@import 'naturalcrit/styles/reset.less'; @import '@sharedStyles/reset.less';
@import 'naturalcrit/styles/elements.less'; @import '@sharedStyles/elements.less';
@import 'naturalcrit/styles/animations.less'; @import '@sharedStyles/animations.less';
@import 'naturalcrit/styles/colors.less'; @import '@sharedStyles/colors.less';
@import 'naturalcrit/styles/tooltip.less'; @import '@sharedStyles/tooltip.less';
@import './themes/fonts/iconFonts/fontAwesome.less'; @import '@themes/fonts/iconFonts/fontAwesome.less';
@import 'font-awesome/css/font-awesome.css';
html,body, #reactContainer, .naturalCrit { min-height : 100%; } html,body, #reactContainer, .naturalCrit { min-height : 100%; }
@@ -84,4 +84,4 @@ const authorLookup = ()=>{
); );
}; };
module.exports = authorLookup; export default authorLookup;
+1 -1
View File
@@ -10,4 +10,4 @@ const authorUtils = ()=>{
); );
}; };
module.exports = authorUtils; export default authorUtils;
@@ -1,9 +1,8 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
import request from 'superagent';
const request = require('superagent'); const BrewCleanup = createReactClass({
const BrewCleanup = createClass({
displayName : 'BrewCleanup', displayName : 'BrewCleanup',
getDefaultProps(){ getDefaultProps(){
return {}; return {};
@@ -69,4 +68,4 @@ const BrewCleanup = createClass({
} }
}); });
module.exports = BrewCleanup; export default BrewCleanup;
@@ -1,8 +1,8 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const request = require('superagent'); import request from 'superagent';
const BrewCompress = createClass({ const BrewCompress = createReactClass({
displayName : 'BrewCompress', displayName : 'BrewCompress',
getDefaultProps(){ getDefaultProps(){
return {}; return {};
@@ -85,4 +85,4 @@ const BrewCompress = createClass({
} }
}); });
module.exports = BrewCompress; export default BrewCompress;
@@ -1,12 +1,11 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const cx = require('classnames'); import request from 'superagent';
import cx from 'classnames';
const request = require('superagent'); import Moment from 'moment';
const Moment = require('moment');
const BrewLookup = createReactClass({
const BrewLookup = createClass({
getDefaultProps() { getDefaultProps() {
return {}; return {};
}, },
@@ -110,4 +109,4 @@ const BrewLookup = createClass({
} }
}); });
module.exports = BrewLookup; export default BrewLookup;
+13 -15
View File
@@ -1,15 +1,14 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import './brewUtils.less';
require('./brewUtils.less');
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx'); import BrewCleanup from './brewCleanup/brewCleanup.jsx';
const BrewLookup = require('./brewLookup/brewLookup.jsx'); import BrewLookup from './brewLookup/brewLookup.jsx';
const BrewCompress = require ('./brewCompress/brewCompress.jsx'); import BrewCompress from './brewCompress/brewCompress.jsx';
const Stats = require('./stats/stats.jsx'); import Stats from './stats/stats.jsx';
const BrewUtils = createClass({ const BrewUtils = ()=>{
render : function(){ return (
return <> <>
<Stats /> <Stats />
<hr /> <hr />
<BrewLookup /> <BrewLookup />
@@ -17,8 +16,7 @@ const BrewUtils = createClass({
<BrewCleanup /> <BrewCleanup />
<hr /> <hr />
<BrewCompress /> <BrewCompress />
</>; </>
} );
}); };
export default BrewUtils;
module.exports = BrewUtils;
+2
View File
@@ -1,3 +1,5 @@
@import '@sharedStyles/colors.less';
.brewUtil { .brewUtil {
.result { .result {
margin-top : 20px; margin-top : 20px;
+5 -6
View File
@@ -1,9 +1,8 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
import request from 'superagent';
const request = require('superagent'); const Stats = createReactClass({
const Stats = createClass({
displayName : 'Stats', displayName : 'Stats',
getDefaultProps(){ getDefaultProps(){
return {}; return {};
@@ -43,4 +42,4 @@ const Stats = createClass({
} }
}); });
module.exports = Stats; export default Stats;
+8 -8
View File
@@ -1,11 +1,11 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
require('./lockTools.less'); import './lockTools.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
import request from '../../homebrew/utils/request-middleware.js'; import request from '../../homebrew/utils/request-middleware.js';
const LockTools = createClass({ const LockTools = createReactClass({
displayName : 'LockTools', displayName : 'LockTools',
getInitialState : function() { getInitialState : function() {
return { return {
@@ -55,7 +55,7 @@ const LockTools = createClass({
} }
}); });
const LockBrew = createClass({ const LockBrew = createReactClass({
displayName : 'LockBrew', displayName : 'LockBrew',
getInitialState : function() { getInitialState : function() {
// Default values // Default values
@@ -183,7 +183,7 @@ const LockBrew = createClass({
} }
}); });
const LockTable = createClass({ const LockTable = createReactClass({
displayName : 'LockTable', displayName : 'LockTable',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -273,7 +273,7 @@ const LockTable = createClass({
} }
}); });
const LockLookup = createClass({ const LockLookup = createReactClass({
displayName : 'LockLookup', displayName : 'LockLookup',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -339,4 +339,4 @@ const LockLookup = createClass({
} }
}); });
module.exports = LockTools; export default LockTools;
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from 'react-dom/client';
import Admin from './admin.jsx';
const props = window.__INITIAL_PROPS__ || {};
createRoot(document.getElementById('reactRoot')).render(<Admin {...props} />);
@@ -1,7 +1,6 @@
require('./notificationAdd.less'); import './notificationAdd.less';
const React = require('react'); import React, { useState, useRef } from 'react';
const { useState, useRef } = require('react'); import request from 'superagent';
const request = require('superagent');
const NotificationAdd = ()=>{ const NotificationAdd = ()=>{
const [notificationResult, setNotificationResult] = useState(null); const [notificationResult, setNotificationResult] = useState(null);
@@ -106,4 +105,4 @@ const NotificationAdd = ()=>{
); );
}; };
module.exports = NotificationAdd; export default NotificationAdd;
@@ -1,9 +1,7 @@
require('./notificationLookup.less'); import './notificationLookup.less';
import React, { useState } from 'react';
const React = require('react'); import request from 'superagent';
const { useState } = require('react'); import Moment from 'moment';
const request = require('superagent');
const Moment = require('moment');
const NotificationDetail = ({ notification, onDelete })=>( const NotificationDetail = ({ notification, onDelete })=>(
<> <>
@@ -102,4 +100,4 @@ const NotificationLookup = ()=>{
); );
}; };
module.exports = NotificationLookup; export default NotificationLookup;
@@ -1,7 +1,6 @@
const React = require('react'); import React from 'react';
import NotificationLookup from './notificationLookup/notificationLookup.jsx';
const NotificationLookup = require('./notificationLookup/notificationLookup.jsx'); import NotificationAdd from './notificationAdd/notificationAdd.jsx';
const NotificationAdd = require('./notificationAdd/notificationAdd.jsx');
const NotificationUtils = ()=>{ const NotificationUtils = ()=>{
return ( return (
@@ -12,4 +11,4 @@ const NotificationUtils = ()=>{
); );
}; };
module.exports = NotificationUtils; export default NotificationUtils;
@@ -1,7 +1,7 @@
import diceFont from 'themes/fonts/iconFonts/diceFont.js'; import diceFont from '@themes/fonts/iconFonts/diceFont.js';
import elderberryInn from 'themes/fonts/iconFonts/elderberryInn.js'; import elderberryInn from '@themes/fonts/iconFonts/elderberryInn.js';
import fontAwesome from 'themes/fonts/iconFonts/fontAwesome.js'; import fontAwesome from '@themes/fonts/iconFonts/fontAwesome.js';
import gameIcons from 'themes/fonts/iconFonts/gameIcons.js'; import gameIcons from '@themes/fonts/iconFonts/gameIcons.js';
const emojis = { const emojis = {
...diceFont, ...diceFont,
@@ -79,6 +79,6 @@ const showAutocompleteEmoji = function(CodeMirror, editor) {
}); });
}; };
module.exports = { export default {
showAutocompleteEmoji showAutocompleteEmoji
}; };
+2 -2
View File
@@ -38,11 +38,11 @@ const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
} }
}; };
module.exports = { export default {
autoCloseCurlyBraces : function(CodeMirror, codeMirror) { autoCloseCurlyBraces : function(CodeMirror, codeMirror) {
const map = { name: 'autoCloseCurlyBraces' }; const map = { name: 'autoCloseCurlyBraces' };
map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); }; map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); };
map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); }; map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); };
codeMirror.addKeyMap(map); codeMirror?.addKeyMap(map);
} }
}; };
+147 -122
View File
@@ -1,55 +1,18 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
require('./codeEditor.less'); import './codeEditor.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash'); import _ from 'lodash';
const closeTag = require('./close-tag'); import closeTag from './close-tag';
const autoCompleteEmoji = require('./autocompleteEmoji'); import autoCompleteEmoji from './autocompleteEmoji';
let CodeMirror; let CodeMirror;
if(typeof window !== 'undefined'){
CodeMirror = require('codemirror');
//Language Modes const CodeEditor = createReactClass({
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
require('codemirror/mode/css/css.js');
require('codemirror/mode/javascript/javascript.js');
//Addons
//Code folding
require('codemirror/addon/fold/foldcode.js');
require('codemirror/addon/fold/foldgutter.js');
//Search and replace
require('codemirror/addon/search/search.js');
require('codemirror/addon/search/searchcursor.js');
require('codemirror/addon/search/jump-to-line.js');
require('codemirror/addon/search/match-highlighter.js');
require('codemirror/addon/search/matchesonscrollbar.js');
require('codemirror/addon/dialog/dialog.js');
//Trailing space highlighting
// require('codemirror/addon/edit/trailingspace.js');
//Active line highlighting
// require('codemirror/addon/selection/active-line.js');
//Scroll past last line
require('codemirror/addon/scroll/scrollpastend.js');
//Auto-closing
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
require('codemirror/addon/fold/xml-fold.js');
require('codemirror/addon/edit/closetag.js');
//Autocompletion
require('codemirror/addon/hint/show-hint.js');
const foldPagesCode = require('./fold-pages');
foldPagesCode.registerHomebreweryHelper(CodeMirror);
const foldCSSCode = require('./fold-css');
foldCSSCode.registerHomebreweryHelper(CodeMirror);
}
const CodeEditor = createClass({
displayName : 'CodeEditor', displayName : 'CodeEditor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
language : '', language : '',
tab : 'brewText',
value : '', value : '',
wrap : true, wrap : true,
onChange : ()=>{}, onChange : ()=>{},
@@ -66,23 +29,54 @@ const CodeEditor = createClass({
editor : React.createRef(null), editor : React.createRef(null),
componentDidMount : function() { async componentDidMount() {
CodeMirror = (await import('codemirror')).default;
this.CodeMirror = CodeMirror;
await import('codemirror/mode/gfm/gfm.js');
await import('codemirror/mode/css/css.js');
await import('codemirror/mode/javascript/javascript.js');
// addons
await import('codemirror/addon/fold/foldcode.js');
await import('codemirror/addon/fold/foldgutter.js');
await import('codemirror/addon/fold/xml-fold.js');
await import('codemirror/addon/search/search.js');
await import('codemirror/addon/search/searchcursor.js');
await import('codemirror/addon/search/jump-to-line.js');
await import('codemirror/addon/search/match-highlighter.js');
await import('codemirror/addon/search/matchesonscrollbar.js');
await import('codemirror/addon/dialog/dialog.js');
await import('codemirror/addon/scroll/scrollpastend.js');
await import('codemirror/addon/edit/closetag.js');
await import('codemirror/addon/hint/show-hint.js');
// import 'codemirror/addon/selection/active-line.js';
// import 'codemirror/addon/edit/trailingspace.js';
// register helpers dynamically as well
const foldPagesCode = (await import('./fold-pages')).default;
const foldCSSCode = (await import('./fold-css')).default;
foldPagesCode.registerHomebreweryHelper(CodeMirror);
foldCSSCode.registerHomebreweryHelper(CodeMirror);
this.buildEditor(); this.buildEditor();
const newDoc = CodeMirror.Doc(this.props.value, this.props.language); const newDoc = CodeMirror?.Doc(this.props.value, this.props.language);
this.codeMirror.swapDoc(newDoc); this.codeMirror?.swapDoc(newDoc);
}, },
componentDidUpdate : function(prevProps) { componentDidUpdate : function(prevProps) {
if(prevProps.view !== this.props.view){ //view changed; swap documents if(prevProps.view !== this.props.view){ //view changed; swap documents
let newDoc; let newDoc;
if(!this.state.docs[this.props.view]) { if(!this.state.docs[this.props.view]) {
newDoc = CodeMirror.Doc(this.props.value, this.props.language); newDoc = CodeMirror?.Doc(this.props.value, this.props.language);
} else { } else {
newDoc = this.state.docs[this.props.view]; newDoc = this.state.docs[this.props.view];
} }
const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) }; const oldDoc = { [prevProps.view]: this.codeMirror?.swapDoc(newDoc) };
this.setState((prevState)=>({ this.setState((prevState)=>({
docs : _.merge({}, prevState.docs, oldDoc) docs : _.merge({}, prevState.docs, oldDoc)
@@ -90,17 +84,17 @@ const CodeEditor = createClass({
this.props.rerenderParent(); this.props.rerenderParent();
} else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside } else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
this.codeMirror.setValue(this.props.value); this.codeMirror?.setValue(this.props.value);
} }
if(this.props.enableFolding) { if(this.props.enableFolding) {
this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror)); this.codeMirror?.setOption('foldOptions', this.foldOptions(this.codeMirror));
} else { } else {
this.codeMirror.setOption('foldOptions', false); this.codeMirror?.setOption('foldOptions', false);
} }
if(prevProps.editorTheme !== this.props.editorTheme){ if(prevProps.editorTheme !== this.props.editorTheme){
this.codeMirror.setOption('theme', this.props.editorTheme); this.codeMirror?.setOption('theme', this.props.editorTheme);
} }
}, },
@@ -188,11 +182,27 @@ const CodeEditor = createClass({
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror); closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror); autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works. // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror?. Either one works.
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());}); this.codeMirror?.on('change', (cm)=>{this.props.onChange(cm.getValue());});
this.updateSize(); this.updateSize();
}, },
// Use for GFM tabs that use common hot-keys
isGFM : function() {
if((this.isGFM()) || (this.props.tab === 'brewSnippets')) return true;
return false;
},
isBrewText : function() {
if(this.isGFM()) return true;
return false;
},
isBrewSnippets : function() {
if(this.props.tab === 'brewSnippets') return true;
return false;
},
indent : function () { indent : function () {
const cm = this.codeMirror; const cm = this.codeMirror;
if(cm.somethingSelected()) { if(cm.somethingSelected()) {
@@ -203,84 +213,94 @@ const CodeEditor = createClass({
}, },
dedent : function () { dedent : function () {
this.codeMirror.execCommand('indentLess'); this.codeMirror?.execCommand('indentLess');
}, },
makeHeader : function (number) { makeHeader : function (number) {
const selection = this.codeMirror.getSelection(); if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection();
const header = Array(number).fill('#').join(''); const header = Array(number).fill('#').join('');
this.codeMirror.replaceSelection(`${header} ${selection}`, 'around'); this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around');
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 });
}, },
makeBold : function() { makeBold : function() {
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**'; if(!this.isGFM()) return;
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around'); const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
} }
}, },
makeItalic : function() { makeItalic : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around'); this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
} }
}, },
makeSuper : function() { makeSuper : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around'); this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
} }
}, },
makeSub : function() { makeSub : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^';
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around'); this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
} }
}, },
makeNbsp : function() { makeNbsp : function() {
this.codeMirror.replaceSelection('&nbsp;', 'end'); if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('&nbsp;', 'end');
}, },
makeSpace : function() { makeSpace : function() {
const selection = this.codeMirror.getSelection(); if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){ if(t){
const percent = parseInt(selection.slice(8, -4)) + 10; const percent = parseInt(selection.slice(8, -4)) + 10;
this.codeMirror.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around'); this.codeMirror?.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around');
} else { } else {
this.codeMirror.replaceSelection(`{{width:10% }}`, 'around'); this.codeMirror?.replaceSelection(`{{width:10% }}`, 'around');
} }
}, },
removeSpace : function() { removeSpace : function() {
const selection = this.codeMirror.getSelection(); if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){ if(t){
const percent = parseInt(selection.slice(8, -4)) - 10; const percent = parseInt(selection.slice(8, -4)) - 10;
this.codeMirror.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around'); this.codeMirror?.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around');
} }
}, },
newColumn : function() { newColumn : function() {
this.codeMirror.replaceSelection('\n\\column\n\n', 'end'); if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('\n\\column\n\n', 'end');
}, },
newPage : function() { newPage : function() {
this.codeMirror.replaceSelection('\n\\page\n\n', 'end'); if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('\n\\page\n\n', 'end');
}, },
injectText : function(injectText, overwrite=true) { injectText : function(injectText, overwrite=true) {
@@ -293,29 +313,32 @@ const CodeEditor = createClass({
}, },
makeUnderline : function() { makeUnderline : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around'); this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
} }
}, },
makeSpan : function() { makeSpan : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around'); this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
} }
}, },
makeDiv : function() { makeDiv : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around'); this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection this.codeMirror?.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection
} }
}, },
@@ -323,8 +346,8 @@ const CodeEditor = createClass({
let regex; let regex;
let cursorPos; let cursorPos;
let newComment; let newComment;
const selection = this.codeMirror.getSelection(); const selection = this.codeMirror?.getSelection();
if(this.props.language === 'gfm'){ if(this.isGFM()){
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs; regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
cursorPos = 4; cursorPos = 4;
newComment = `<!-- ${selection} -->`; newComment = `<!-- ${selection} -->`;
@@ -333,44 +356,46 @@ const CodeEditor = createClass({
cursorPos = 3; cursorPos = 3;
newComment = `/* ${selection} */`; newComment = `/* ${selection} */`;
} }
this.codeMirror.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around'); this.codeMirror?.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos }); this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos });
}; };
}, },
makeLink : function() { makeLink : function() {
if(!this.isGFM()) return;
const isLink = /^\[(.*)\]\((.*)\)$/; const isLink = /^\[(.*)\]\((.*)\)$/;
const selection = this.codeMirror.getSelection().trim(); const selection = this.codeMirror?.getSelection().trim();
let match; let match;
if(match = isLink.exec(selection)){ if(match = isLink.exec(selection)){
const altText = match[1]; const altText = match[1];
const url = match[2]; const url = match[2];
this.codeMirror.replaceSelection(`${altText} ${url}`); this.codeMirror?.replaceSelection(`${altText} ${url}`);
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch }); this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch });
} else { } else {
this.codeMirror.replaceSelection(`[${selection || 'alt text'}](url)`); this.codeMirror?.replaceSelection(`[${selection || 'alt text'}](url)`);
const cursor = this.codeMirror.getCursor(); const cursor = this.codeMirror?.getCursor();
this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 }); this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 });
} }
}, },
makeList : function(listType) { makeList : function(listType) {
if(!this.isGFM()) return;
const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to'); const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to');
this.codeMirror.setSelection( this.codeMirror?.setSelection(
{ line: selectionStart.line, ch: 0 }, { line: selectionStart.line, ch: 0 },
{ line: selectionEnd.line, ch: this.codeMirror.getLine(selectionEnd.line).length } { line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length }
); );
const newSelection = this.codeMirror.getSelection(); const newSelection = this.codeMirror?.getSelection();
const regex = /^\d+\.\s|^-\s/gm; const regex = /^\d+\.\s|^-\s/gm;
if(newSelection.match(regex) != null){ // if selection IS A LIST if(newSelection.match(regex) != null){ // if selection IS A LIST
this.codeMirror.replaceSelection(newSelection.replace(regex, ''), 'around'); this.codeMirror?.replaceSelection(newSelection.replace(regex, ''), 'around');
} else { // if selection IS NOT A LIST } else { // if selection IS NOT A LIST
listType == 'UL' ? this.codeMirror.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') : listType == 'UL' ? this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') :
this.codeMirror.replaceSelection(newSelection.replace(/^/gm, (()=>{ this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, (()=>{
let n = 1; let n = 1;
return ()=>{ return ()=>{
return `${n++}. `; return `${n++}. `;
@@ -380,39 +405,39 @@ const CodeEditor = createClass({
}, },
foldAllCode : function() { foldAllCode : function() {
this.codeMirror.execCommand('foldAll'); this.codeMirror?.execCommand('foldAll');
}, },
unfoldAllCode : function() { unfoldAllCode : function() {
this.codeMirror.execCommand('unfoldAll'); this.codeMirror?.execCommand('unfoldAll');
}, },
//=-- Externally used -==// //=-- Externally used -==//
setCursorPosition : function(line, char){ setCursorPosition : function(line, char){
setTimeout(()=>{ setTimeout(()=>{
this.codeMirror.focus(); this.codeMirror?.focus();
this.codeMirror.doc.setCursor(line, char); this.codeMirror?.doc.setCursor(line, char);
}, 10); }, 10);
}, },
getCursorPosition : function(){ getCursorPosition : function(){
return this.codeMirror.getCursor(); return this.codeMirror?.getCursor();
}, },
getTopVisibleLine : function(){ getTopVisibleLine : function(){
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect(); const rect = this.codeMirror?.getWrapperElement().getBoundingClientRect();
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window'); const topVisibleLine = this.codeMirror?.lineAtHeight(rect.top, 'window');
return topVisibleLine; return topVisibleLine;
}, },
updateSize : function(){ updateSize : function(){
this.codeMirror.refresh(); this.codeMirror?.refresh();
}, },
redo : function(){ redo : function(){
return this.codeMirror.redo(); return this.codeMirror?.redo();
}, },
undo : function(){ undo : function(){
return this.codeMirror.undo(); return this.codeMirror?.undo();
}, },
historySize : function(){ historySize : function(){
return this.codeMirror.doc.historySize(); return this.codeMirror?.doc.historySize();
}, },
foldOptions : function(cm){ foldOptions : function(cm){
@@ -426,7 +451,7 @@ const CodeEditor = createClass({
let foldPreviewText = ''; let foldPreviewText = '';
while (currentLine <= to.line && text.length <= maxLength) { while (currentLine <= to.line && text.length <= maxLength) {
const currentText = this.codeMirror.getLine(currentLine); const currentText = this.codeMirror?.getLine(currentLine);
currentLine++; currentLine++;
if(currentText[0] == '#'){ if(currentText[0] == '#'){
foldPreviewText = currentText; foldPreviewText = currentText;
@@ -461,5 +486,5 @@ const CodeEditor = createClass({
} }
}); });
module.exports = CodeEditor; export default CodeEditor;
+4 -4
View File
@@ -5,10 +5,10 @@
@import (less) 'codemirror/addon/hint/show-hint.css'; @import (less) 'codemirror/addon/hint/show-hint.css';
//Icon fonts included so they can appear in emoji autosuggest dropdown //Icon fonts included so they can appear in emoji autosuggest dropdown
@import (less) './themes/fonts/iconFonts/diceFont.less'; @import (less) '@themes/fonts/iconFonts/diceFont.less';
@import (less) './themes/fonts/iconFonts/elderberryInn.less'; @import (less) '@themes/fonts/iconFonts/elderberryInn.less';
@import (less) './themes/fonts/iconFonts/gameIcons.less'; @import (less) '@themes/fonts/iconFonts/gameIcons.less';
@import (less) './themes/fonts/iconFonts/fontAwesome.less'; @import (less) '@themes/fonts/iconFonts/fontAwesome.less';
@keyframes sourceMoveAnimation { @keyframes sourceMoveAnimation {
50% { color : white;background-color : red;} 50% { color : white;background-color : red;}
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = { export default {
registerHomebreweryHelper : function(CodeMirror) { registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) { CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = { export default {
registerHomebreweryHelper : function(CodeMirror) { registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) { CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
const matcher = /^\\page.*/; const matcher = /^\\page.*/;
+21 -9
View File
@@ -1,9 +1,9 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash'); import _ from 'lodash';
require('./combobox.less'); import './combobox.less';
const Combobox = createClass({ const Combobox = createReactClass({
displayName : 'Combobox', displayName : 'Combobox',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -11,14 +11,17 @@ const Combobox = createClass({
trigger : 'hover', trigger : 'hover',
default : '', default : '',
placeholder : '', placeholder : '',
tooltip : '',
autoSuggest : { autoSuggest : {
clearAutoSuggestOnClick : true, clearAutoSuggestOnClick : true,
suggestMethod : 'includes', suggestMethod : 'includes',
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
}, },
valuePatterns : /.+/
}; };
}, },
getInitialState : function() { getInitialState : function() {
this.dropdownRef = React.createRef();
return { return {
showDropdown : false, showDropdown : false,
value : '', value : '',
@@ -39,7 +42,7 @@ const Combobox = createClass({
}, },
handleClickOutside : function(e){ handleClickOutside : function(e){
// Close dropdown when clicked outside // Close dropdown when clicked outside
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) { if(this.dropdownRef.current && !this.dropdownRef.current.contains(e.target)) {
this.handleDropdown(false); this.handleDropdown(false);
} }
}, },
@@ -69,11 +72,14 @@ const Combobox = createClass({
return ( return (
<div className='dropdown-input item' <div className='dropdown-input item'
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined} onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}> onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
{...(this.props.tooltip ? { 'data-tooltip-right': this.props.tooltip } : {})}>
<input <input
type='text' type='text'
onChange={(e)=>this.handleInput(e)} onChange={(e)=>this.handleInput(e)}
value={this.state.value || ''} value={this.state.value || ''}
title=''
pattern={this.props.valuePatterns}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
onBlur={(e)=>{ onBlur={(e)=>{
if(!e.target.checkValidity()){ if(!e.target.checkValidity()){
@@ -82,6 +88,12 @@ const Combobox = createClass({
}); });
} }
}} }}
onKeyDown={(e)=>{
if(e.key === 'Enter') {
e.preventDefault();
this.props.onEntry(e);
}
}}
/> />
<i className='fas fa-caret-down'/> <i className='fas fa-caret-down'/>
</div> </div>
@@ -117,7 +129,7 @@ const Combobox = createClass({
}); });
return ( return (
<div className={`dropdown-container ${this.props.className}`} <div className={`dropdown-container ${this.props.className}`}
ref='dropdown' ref={this.dropdownRef}
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}> onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
{this.renderTextInput()} {this.renderTextInput()}
{this.renderDropdown(dropdownChildren)} {this.renderDropdown(dropdownChildren)}
@@ -126,4 +138,4 @@ const Combobox = createClass({
} }
}); });
module.exports = Combobox; export default Combobox;
+1
View File
@@ -10,6 +10,7 @@
position : absolute; position : absolute;
z-index : 100; z-index : 100;
width : 100%; width : 100%;
height : max-content;
max-height : 200px; max-height : 200px;
overflow-y : auto; overflow-y : auto;
background-color : white; background-color : white;
@@ -1,11 +1,11 @@
require('./renderWarnings.less'); import './renderWarnings.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash'); import _ from 'lodash';
import Dialog from '../dialog.jsx'; import Dialog from '../dialog.jsx';
const RenderWarnings = createClass({ const RenderWarnings = createReactClass({
displayName : 'RenderWarnings', displayName : 'RenderWarnings',
getInitialState : function() { getInitialState : function() {
return { return {
@@ -57,4 +57,4 @@ const RenderWarnings = createClass({
} }
}); });
module.exports = RenderWarnings; export default RenderWarnings;
@@ -1,3 +1,5 @@
@import '@sharedStyles/colors.less';
.renderWarnings { .renderWarnings {
position : relative; position : relative;
float : right; float : right;
+3 -4
View File
@@ -1,6 +1,5 @@
require('./splitPane.less'); import './splitPane.less';
const React = require('react'); import React, { useEffect, useState } from 'react';
const { useState, useEffect } = React;
const PANE_WIDTH_KEY = 'HB_editor_splitWidth'; const PANE_WIDTH_KEY = 'HB_editor_splitWidth';
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll'; const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
@@ -108,4 +107,4 @@ const Pane = ({ width, children, isDragging, moveBrew, moveSource, liveScroll, s
); );
}; };
module.exports = SplitPane; export default SplitPane;
@@ -1,3 +1,4 @@
@import '@sharedStyles/core.less';
.splitPane { .splitPane {
position : relative; position : relative;
+2 -3
View File
@@ -1,7 +1,6 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class');
module.exports = function(props){ export default function(props){
return <svg version='1.1' x='0px' y='0px' viewBox='0 0 90 112.5' enableBackground='new 0 0 90 90' > return <svg version='1.1' x='0px' y='0px' viewBox='0 0 90 112.5' enableBackground='new 0 0 90 90' >
<path d='M25.363,25.54c0,1.906,8.793,3.454,19.636,3.454c10.848,0,19.638-1.547,19.638-3.454c0-1.12-3.056-2.117-7.774-2.75 c-1.418,1.891-3.659,3.133-6.208,3.133c-2.85,0-5.315-1.547-6.67-3.833C33.617,22.185,25.363,23.692,25.363,25.54z'/><path d='M84.075,54.142c0-8.68-2.868-17.005-8.144-23.829c1.106-1.399,1.41-2.771,1.41-3.854c0-6.574-10.245-9.358-19.264-10.533 c0.209,0.706,0.359,1.439,0.359,2.215c0,0.09-0.022,0.17-0.028,0.26l0,0c-0.028,0.853-0.195,1.667-0.479,2.429 c9.106,1.282,14.508,3.754,14.508,5.63c0,2.644-10.688,6.486-27.439,6.486c-16.748,0-27.438-3.842-27.438-6.486 c0-2.542,9.904-6.183,25.559-6.459c-0.098-0.396-0.159-0.807-0.2-1.223c0.006,0,0.013,0,0.017,0 c-0.017-0.213-0.063-0.417-0.063-0.636c0-1.084,0.226-2.119,0.628-3.058c-6.788,0.129-30.846,1.299-30.846,11.376 c0,1.083,0.305,2.455,1.411,3.854c-5.276,6.823-8.145,15.149-8.145,23.829c0,11.548,5.187,20.107,14.693,25.115 c-0.902,3.146-1.391,7.056,1.111,8.181c2.626,1.178,5.364-2.139,7.111-5.005c4.73,1.261,10.13,1.923,16.161,1.923 c6.034,0,11.428-0.661,16.158-1.922c1.75,2.865,4.493,6.18,7.112,5.004c2.504-1.123,2.014-5.035,1.113-8.179 C78.889,74.249,84.075,65.689,84.075,54.142z M70.39,31.392c5.43,6.046,8.78,14,8.78,22.75c0,20.919-18.582,25.309-34.171,25.309 c-15.587,0-34.17-4.39-34.17-25.309c0-8.75,3.35-16.7,8.781-22.753c5.561,2.643,15.502,4.009,25.389,4.009 C54.886,35.397,64.829,34.031,70.39,31.392z'/><path d='M50.654,23.374c2.892,0,5.234-2.341,5.234-5.233c0-2.887-2.343-5.23-5.234-5.23c-2.887,0-5.231,2.343-5.231,5.23 C45.423,21.032,47.768,23.374,50.654,23.374z'/> <path d='M25.363,25.54c0,1.906,8.793,3.454,19.636,3.454c10.848,0,19.638-1.547,19.638-3.454c0-1.12-3.056-2.117-7.774-2.75 c-1.418,1.891-3.659,3.133-6.208,3.133c-2.85,0-5.315-1.547-6.67-3.833C33.617,22.185,25.363,23.692,25.363,25.54z'/><path d='M84.075,54.142c0-8.68-2.868-17.005-8.144-23.829c1.106-1.399,1.41-2.771,1.41-3.854c0-6.574-10.245-9.358-19.264-10.533 c0.209,0.706,0.359,1.439,0.359,2.215c0,0.09-0.022,0.17-0.028,0.26l0,0c-0.028,0.853-0.195,1.667-0.479,2.429 c9.106,1.282,14.508,3.754,14.508,5.63c0,2.644-10.688,6.486-27.439,6.486c-16.748,0-27.438-3.842-27.438-6.486 c0-2.542,9.904-6.183,25.559-6.459c-0.098-0.396-0.159-0.807-0.2-1.223c0.006,0,0.013,0,0.017,0 c-0.017-0.213-0.063-0.417-0.063-0.636c0-1.084,0.226-2.119,0.628-3.058c-6.788,0.129-30.846,1.299-30.846,11.376 c0,1.083,0.305,2.455,1.411,3.854c-5.276,6.823-8.145,15.149-8.145,23.829c0,11.548,5.187,20.107,14.693,25.115 c-0.902,3.146-1.391,7.056,1.111,8.181c2.626,1.178,5.364-2.139,7.111-5.005c4.73,1.261,10.13,1.923,16.161,1.923 c6.034,0,11.428-0.661,16.158-1.922c1.75,2.865,4.493,6.18,7.112,5.004c2.504-1.123,2.014-5.035,1.113-8.179 C78.889,74.249,84.075,65.689,84.075,54.142z M70.39,31.392c5.43,6.046,8.78,14,8.78,22.75c0,20.919-18.582,25.309-34.171,25.309 c-15.587,0-34.17-4.39-34.17-25.309c0-8.75,3.35-16.7,8.781-22.753c5.561,2.643,15.502,4.009,25.389,4.009 C54.886,35.397,64.829,34.031,70.39,31.392z'/><path d='M50.654,23.374c2.892,0,5.234-2.341,5.234-5.233c0-2.887-2.343-5.23-5.234-5.23c-2.887,0-5.231,2.343-5.231,5.23 C45.423,21.032,47.768,23.374,50.654,23.374z'/>
<circle cx='62.905' cy='10.089' r='3.595'/> <circle cx='62.905' cy='10.089' r='3.595'/>
@@ -1,6 +1,5 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class');
module.exports = function(props){ export default function(props){
return <svg version='1.1' x='0px' y='0px' viewBox='0 0 100 100' enableBackground='new 0 0 100 100'><path d='M80.644,87.982l16.592-41.483c0.054-0.128,0.088-0.26,0.108-0.394c0.006-0.039,0.007-0.077,0.011-0.116 c0.007-0.087,0.008-0.174,0.002-0.26c-0.003-0.046-0.007-0.091-0.014-0.137c-0.014-0.089-0.036-0.176-0.063-0.262 c-0.012-0.034-0.019-0.069-0.031-0.103c-0.047-0.118-0.106-0.229-0.178-0.335c-0.004-0.006-0.006-0.012-0.01-0.018L67.999,3.358 c-0.01-0.013-0.003-0.026-0.013-0.04L68,3.315V4c0,0-0.033,0-0.037,0c-0.403-1-1.094-1.124-1.752-0.976 c0,0.004-0.004-0.012-0.007-0.012C66.201,3.016,66.194,3,66.194,3H66.19h-0.003h-0.003h-0.004h-0.003c0,0-0.004,0-0.007,0 s-0.003-0.151-0.007-0.151L20.495,15.227c-0.025,0.007-0.046-0.019-0.071-0.011c-0.087,0.028-0.172,0.041-0.253,0.083 c-0.054,0.027-0.102,0.053-0.152,0.085c-0.051,0.033-0.101,0.061-0.147,0.099c-0.044,0.036-0.084,0.073-0.124,0.113 c-0.048,0.048-0.093,0.098-0.136,0.152c-0.03,0.039-0.059,0.076-0.085,0.117c-0.046,0.07-0.084,0.145-0.12,0.223 c-0.011,0.023-0.027,0.042-0.036,0.066L2.911,57.664C2.891,57.715,3,57.768,3,57.82v0.002c0,0.186,0,0.375,0,0.562 c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004v0.002c0,0.074-0.002,0.15,0.012,0.223 C3.015,58.631,3,58.631,3,58.633c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004c0,0,0,0,0,0.002v0.004 c0,0.191-0.046,0.377,0.06,0.545c0-0.002-0.03,0.004-0.03,0.004c0,0.004-0.03,0.004-0.03,0.004c0,0.002,0,0.002,0,0.002 l-0.045,0.004c0.03,0.047,0.036,0.09,0.068,0.133l29.049,37.359c0.002,0.004,0,0.006,0.002,0.01c0.002,0.002,0,0.004,0.002,0.008 c0.006,0.008,0.014,0.014,0.021,0.021c0.024,0.029,0.052,0.051,0.078,0.078c0.027,0.029,0.053,0.057,0.082,0.082 c0.03,0.027,0.055,0.062,0.086,0.088c0.026,0.02,0.057,0.033,0.084,0.053c0.04,0.027,0.081,0.053,0.123,0.076 c0.005,0.004,0.01,0.008,0.016,0.01c0.087,0.051,0.176,0.09,0.269,0.123c0.042,0.014,0.082,0.031,0.125,0.043 c0.021,0.006,0.041,0.018,0.062,0.021c0.123,0.027,0.249,0.043,0.375,0.043c0.099,0,0.202-0.012,0.304-0.027l45.669-8.303 c0.057-0.01,0.108-0.021,0.163-0.037C79.547,88.992,79.562,89,79.575,89c0.004,0,0.004,0,0.004,0c0.021,0,0.039-0.027,0.06-0.035 c0.041-0.014,0.08-0.034,0.12-0.052c0.021-0.01,0.044-0.019,0.064-0.03c0.017-0.01,0.026-0.015,0.033-0.017 c0.014-0.008,0.023-0.021,0.037-0.028c0.14-0.078,0.269-0.174,0.38-0.285c0.014-0.016,0.024-0.034,0.038-0.048 c0.109-0.119,0.201-0.252,0.271-0.398c0.006-0.01,0.016-0.018,0.021-0.029c0.004-0.008,0.008-0.017,0.011-0.026 c0.002-0.004,0.003-0.006,0.005-0.01C80.627,88.021,80.635,88.002,80.644,87.982z M77.611,84.461L48.805,66.453l32.407-25.202 L77.611,84.461z M46.817,63.709L35.863,23.542l43.818,14.608L46.817,63.709z M84.668,40.542l8.926,5.952l-11.902,29.75 L84.668,40.542z M89.128,39.446L84.53,36.38l-6.129-12.257L89.128,39.446z M79.876,34.645L37.807,20.622L65.854,6.599L79.876,34.645 z M33.268,19.107l-6.485-2.162l23.781-6.487L33.268,19.107z M21.92,18.895l8.67,2.891L10.357,47.798L21.92,18.895z M32.652,24.649 l10.845,39.757L7.351,57.178L32.652,24.649z M43.472,67.857L32.969,92.363L8.462,60.855L43.472,67.857z M46.631,69.09l27.826,17.393 l-38.263,6.959L46.631,69.09z'></path></svg>; return <svg version='1.1' x='0px' y='0px' viewBox='0 0 100 100' enableBackground='new 0 0 100 100'><path d='M80.644,87.982l16.592-41.483c0.054-0.128,0.088-0.26,0.108-0.394c0.006-0.039,0.007-0.077,0.011-0.116 c0.007-0.087,0.008-0.174,0.002-0.26c-0.003-0.046-0.007-0.091-0.014-0.137c-0.014-0.089-0.036-0.176-0.063-0.262 c-0.012-0.034-0.019-0.069-0.031-0.103c-0.047-0.118-0.106-0.229-0.178-0.335c-0.004-0.006-0.006-0.012-0.01-0.018L67.999,3.358 c-0.01-0.013-0.003-0.026-0.013-0.04L68,3.315V4c0,0-0.033,0-0.037,0c-0.403-1-1.094-1.124-1.752-0.976 c0,0.004-0.004-0.012-0.007-0.012C66.201,3.016,66.194,3,66.194,3H66.19h-0.003h-0.003h-0.004h-0.003c0,0-0.004,0-0.007,0 s-0.003-0.151-0.007-0.151L20.495,15.227c-0.025,0.007-0.046-0.019-0.071-0.011c-0.087,0.028-0.172,0.041-0.253,0.083 c-0.054,0.027-0.102,0.053-0.152,0.085c-0.051,0.033-0.101,0.061-0.147,0.099c-0.044,0.036-0.084,0.073-0.124,0.113 c-0.048,0.048-0.093,0.098-0.136,0.152c-0.03,0.039-0.059,0.076-0.085,0.117c-0.046,0.07-0.084,0.145-0.12,0.223 c-0.011,0.023-0.027,0.042-0.036,0.066L2.911,57.664C2.891,57.715,3,57.768,3,57.82v0.002c0,0.186,0,0.375,0,0.562 c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004v0.002c0,0.074-0.002,0.15,0.012,0.223 C3.015,58.631,3,58.631,3,58.633c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004c0,0,0,0,0,0.002v0.004 c0,0.191-0.046,0.377,0.06,0.545c0-0.002-0.03,0.004-0.03,0.004c0,0.004-0.03,0.004-0.03,0.004c0,0.002,0,0.002,0,0.002 l-0.045,0.004c0.03,0.047,0.036,0.09,0.068,0.133l29.049,37.359c0.002,0.004,0,0.006,0.002,0.01c0.002,0.002,0,0.004,0.002,0.008 c0.006,0.008,0.014,0.014,0.021,0.021c0.024,0.029,0.052,0.051,0.078,0.078c0.027,0.029,0.053,0.057,0.082,0.082 c0.03,0.027,0.055,0.062,0.086,0.088c0.026,0.02,0.057,0.033,0.084,0.053c0.04,0.027,0.081,0.053,0.123,0.076 c0.005,0.004,0.01,0.008,0.016,0.01c0.087,0.051,0.176,0.09,0.269,0.123c0.042,0.014,0.082,0.031,0.125,0.043 c0.021,0.006,0.041,0.018,0.062,0.021c0.123,0.027,0.249,0.043,0.375,0.043c0.099,0,0.202-0.012,0.304-0.027l45.669-8.303 c0.057-0.01,0.108-0.021,0.163-0.037C79.547,88.992,79.562,89,79.575,89c0.004,0,0.004,0,0.004,0c0.021,0,0.039-0.027,0.06-0.035 c0.041-0.014,0.08-0.034,0.12-0.052c0.021-0.01,0.044-0.019,0.064-0.03c0.017-0.01,0.026-0.015,0.033-0.017 c0.014-0.008,0.023-0.021,0.037-0.028c0.14-0.078,0.269-0.174,0.38-0.285c0.014-0.016,0.024-0.034,0.038-0.048 c0.109-0.119,0.201-0.252,0.271-0.398c0.006-0.01,0.016-0.018,0.021-0.029c0.004-0.008,0.008-0.017,0.011-0.026 c0.002-0.004,0.003-0.006,0.005-0.01C80.627,88.021,80.635,88.002,80.644,87.982z M77.611,84.461L48.805,66.453l32.407-25.202 L77.611,84.461z M46.817,63.709L35.863,23.542l43.818,14.608L46.817,63.709z M84.668,40.542l8.926,5.952l-11.902,29.75 L84.668,40.542z M89.128,39.446L84.53,36.38l-6.129-12.257L89.128,39.446z M79.876,34.645L37.807,20.622L65.854,6.599L79.876,34.645 z M33.268,19.107l-6.485-2.162l23.781-6.487L33.268,19.107z M21.92,18.895l8.67,2.891L10.357,47.798L21.92,18.895z M32.652,24.649 l10.845,39.757L7.351,57.178L32.652,24.649z M43.472,67.857L32.969,92.363L8.462,60.855L43.472,67.857z M46.631,69.09l27.826,17.393 l-38.263,6.959L46.631,69.09z'></path></svg>;
}; };
+18 -15
View File
@@ -1,23 +1,24 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less'); import brewRendererStylesUrl from './brewRenderer.less?url';
const React = require('react'); import headerNavStylesUrl from './headerNav/headerNav.less?url';
const { useState, useRef, useMemo, useEffect } = React; import './brewRenderer.less';
const _ = require('lodash'); import React, { useState, useRef, useMemo, useEffect } from 'react';
import _ from 'lodash';
const MarkdownLegacy = require('markdownLegacy.js'); import MarkdownLegacy from '@shared/markdownLegacy.js';
import Markdown from 'markdown.js'; import Markdown from '@shared/markdown.js';
const ErrorBar = require('./errorBar/errorBar.jsx'); import ErrorBar from './errorBar/errorBar.jsx';
const ToolBar = require('./toolBar/toolBar.jsx'); import ToolBar from './toolBar/toolBar.jsx';
//TODO: move to the brew renderer //TODO: move to the brew renderer
const RenderWarnings = require('client/components/renderWarnings/renderWarnings.jsx'); import RenderWarnings from '../../components/renderWarnings/renderWarnings.jsx';
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx'); import NotificationPopup from './notificationPopup/notificationPopup.jsx';
const Frame = require('react-frame-component').default; import Frame from 'react-frame-component';
const dedent = require('dedent-tabs').default; import dedent from 'dedent';
const { printCurrentBrew } = require('../../../shared/helpers.js'); import { printCurrentBrew } from '@shared/helpers.js';
import HeaderNav from './headerNav/headerNav.jsx'; import HeaderNav from './headerNav/headerNav.jsx';
import { safeHTML } from './safeHTML.js'; import safeHTML from './safeHTML.js';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m; const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
@@ -30,6 +31,8 @@ const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head> <!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> <link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' /> <link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
<link href="${headerNavStylesUrl}" rel="stylesheet" />
<base target=_blank> <base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`; </head><body style='overflow: hidden'><div></div></body></html>`;
@@ -345,4 +348,4 @@ const BrewRenderer = (props)=>{
); );
}; };
module.exports = BrewRenderer; export default BrewRenderer;
@@ -1,4 +1,4 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less'; @import '@sharedStyles/core.less';
.brewRenderer { .brewRenderer {
height : 100vh; height : 100vh;
@@ -59,6 +59,12 @@
} }
&-corner { visibility : hidden; } &-corner { visibility : hidden; }
} }
@supports (break-after:always) {
.columnSplit {
margin-bottom: 100vh;
}
}
} }
.pane { position : relative; } .pane { position : relative; }
@@ -81,4 +87,5 @@
} }
} }
.headerNav { visibility : hidden; } .headerNav { visibility : hidden; }
}
}
@@ -1,5 +1,5 @@
require('./errorBar.less'); import './errorBar.less';
const React = require('react'); import React from 'react';
import Dialog from '../../../components/dialog.jsx'; import Dialog from '../../../components/dialog.jsx';
@@ -50,4 +50,4 @@ const ErrorBar = (props)=>{
); );
}; };
module.exports = ErrorBar; export default ErrorBar;
@@ -1,3 +1,4 @@
@import '@sharedStyles/colors.less';
.errorBar { .errorBar {
position : absolute; position : absolute;
@@ -1,7 +1,7 @@
require('./headerNav.less'); import './headerNav.less';
import * as React from 'react'; import React from 'react';
import * as _ from 'lodash'; import _ from 'lodash';
const MAX_TEXT_LENGTH = 40; const MAX_TEXT_LENGTH = 40;
@@ -1,7 +1,7 @@
require('./notificationPopup.less'); import './notificationPopup.less';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'markdown.js'; import Markdown from '@shared/markdown.js';
import Dialog from '../../../components/dialog.jsx'; import Dialog from '../../../components/dialog.jsx';
@@ -62,4 +62,4 @@ const NotificationPopup = ()=>{
</Dialog>; </Dialog>;
}; };
module.exports = NotificationPopup; export default NotificationPopup;
@@ -1,3 +1,5 @@
@import './client/homebrew/navbar/navbar.less';
.popups { .popups {
position : fixed; position : fixed;
top : calc(@navbarHeight + @viewerToolsHeight); top : calc(@navbarHeight + @viewerToolsHeight);
+1 -1
View File
@@ -43,4 +43,4 @@ function safeHTML(htmlString) {
return div.innerHTML; return div.innerHTML;
}; };
module.exports.safeHTML = safeHTML; export default safeHTML;
@@ -1,8 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
require('./toolBar.less'); import './toolBar.less';
const React = require('react'); import React, { useState, useEffect } from 'react';
const { useState, useEffect } = React; import _ from 'lodash';
const _ = require('lodash');
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx'; import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
@@ -100,18 +99,18 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
return ( return (
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'> <div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
<div className='toggleButton'> <div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{ <button data-tooltip-right={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
setToolsVisible(!toolsVisible); setToolsVisible(!toolsVisible);
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible); localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible);
}}><i className='fas fa-glasses' /></button> }}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button> <button data-tooltip-right={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div> </div>
{/*v=====----------------------< Zoom Controls >---------------------=====v*/} {/*v=====----------------------< Zoom Controls >---------------------=====v*/}
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}> <div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
<button <button
id='fill-width' id='fill-width'
className='tool' className='tool'
title='Set zoom to fill preview with one page' data-tooltip-bottom='Set zoom to fill preview with one page'
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fill'))} onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fill'))}
> >
<i className='fac fit-width' /> <i className='fac fit-width' />
@@ -119,7 +118,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<button <button
id='zoom-to-fit' id='zoom-to-fit'
className='tool' className='tool'
title='Set zoom to fit entire page in preview' data-tooltip-bottom='Set zoom to fit entire page in preview'
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fit'))} onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fit'))}
> >
<i className='fac zoom-to-fit' /> <i className='fac zoom-to-fit' />
@@ -129,7 +128,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='tool' className='tool'
onClick={()=>handleZoomButton(displayOptions.zoomLevel - 20)} onClick={()=>handleZoomButton(displayOptions.zoomLevel - 20)}
disabled={displayOptions.zoomLevel <= MIN_ZOOM} disabled={displayOptions.zoomLevel <= MIN_ZOOM}
title='Zoom Out' data-tooltip-bottom='Zoom Out'
> >
<i className='fas fa-magnifying-glass-minus' /> <i className='fas fa-magnifying-glass-minus' />
</button> </button>
@@ -138,7 +137,6 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='range-input tool hover-tooltip' className='range-input tool hover-tooltip'
type='range' type='range'
name='zoom' name='zoom'
title='Set Zoom'
list='zoomLevels' list='zoomLevels'
min={MIN_ZOOM} min={MIN_ZOOM}
max={MAX_ZOOM} max={MAX_ZOOM}
@@ -155,7 +153,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='tool' className='tool'
onClick={()=>handleZoomButton(displayOptions.zoomLevel + 20)} onClick={()=>handleZoomButton(displayOptions.zoomLevel + 20)}
disabled={displayOptions.zoomLevel >= MAX_ZOOM} disabled={displayOptions.zoomLevel >= MAX_ZOOM}
title='Zoom In' data-tooltip-bottom='Zoom In'
> >
<i className='fas fa-magnifying-glass-plus' /> <i className='fas fa-magnifying-glass-plus' />
</button> </button>
@@ -167,44 +165,44 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<button role='radio' <button role='radio'
id='single-spread' id='single-spread'
className='tool' className='tool'
title='Single Page' data-tooltip-bottom='Single Page'
onClick={()=>{handleOptionChange('spread', 'single');}} onClick={()=>{handleOptionChange('spread', 'single');}}
aria-checked={displayOptions.spread === 'single'} aria-checked={displayOptions.spread === 'single'}
><i className='fac single-spread' /></button> ><i className='fac single-spread' /></button>
<button role='radio' <button role='radio'
id='facing-spread' id='facing-spread'
className='tool' className='tool'
title='Facing Pages' data-tooltip-bottom='Facing Pages'
onClick={()=>{handleOptionChange('spread', 'facing');}} onClick={()=>{handleOptionChange('spread', 'facing');}}
aria-checked={displayOptions.spread === 'facing'} aria-checked={displayOptions.spread === 'facing'}
><i className='fac facing-spread' /></button> ><i className='fac facing-spread' /></button>
<button role='radio' <button role='radio'
id='flow-spread' id='flow-spread'
className='tool' className='tool'
title='Flow Pages' data-tooltip-bottom='Flow Pages'
onClick={()=>{handleOptionChange('spread', 'flow');}} onClick={()=>{handleOptionChange('spread', 'flow');}}
aria-checked={displayOptions.spread === 'flow'} aria-checked={displayOptions.spread === 'flow'}
><i className='fac flow-spread' /></button> ><i className='fac flow-spread' /></button>
</div> </div>
<Anchored> <Anchored>
<AnchoredTrigger id='spread-settings' className='tool' title='Spread options'><i className='fas fa-gear' /></AnchoredTrigger> <AnchoredTrigger id='spread-settings' className='tool' data-tooltip-bottom='Spread options'><i className='fas fa-gear' /></AnchoredTrigger>
<AnchoredBox title='Options'> <AnchoredBox>
<h1>Options</h1> <h1>Options</h1>
<label title='Modify the horizontal space between pages.'> <label data-tooltip-left='Modify the horizontal space between pages.'>
Column gap Column gap
<input type='range' min={0} max={200} defaultValue={displayOptions.columnGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} /> <input type='range' min={0} max={200} defaultValue={displayOptions.columnGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
</label> </label>
<label title='Modify the vertical space between rows of pages.'> <label data-tooltip-left='Modify the vertical space between rows of pages.'>
Row gap Row gap
<input type='range' min={0} max={200} defaultValue={displayOptions.rowGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} /> <input type='range' min={0} max={200} defaultValue={displayOptions.rowGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
</label> </label>
<label title='Start 1st page on the right side, such as if you have cover page.'> <label data-tooltip-left='Start 1st page on the right side, such as if you have cover page.'>
Start on right Start on right
<input type='checkbox' checked={displayOptions.startOnRight} onChange={()=>{handleOptionChange('startOnRight', !displayOptions.startOnRight);}} <input type='checkbox' checked={displayOptions.startOnRight} onChange={()=>{handleOptionChange('startOnRight', !displayOptions.startOnRight);}}
title={displayOptions.spread !== 'facing' ? 'Switch to Facing to enable toggle.' : null} /> data-tooltip-right={displayOptions.spread !== 'facing' ? 'Switch to Facing to enable toggle.' : null} />
</label> </label>
<label title='Toggle the page shadow on every page.'> <label data-tooltip-left='Toggle the page shadow on every page.'>
Page shadows Page shadows
<input type='checkbox' checked={displayOptions.pageShadows} onChange={()=>{handleOptionChange('pageShadows', !displayOptions.pageShadows);}} /> <input type='checkbox' checked={displayOptions.pageShadows} onChange={()=>{handleOptionChange('pageShadows', !displayOptions.pageShadows);}} />
</label> </label>
@@ -218,7 +216,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
id='previous-page' id='previous-page'
className='previousPage tool' className='previousPage tool'
type='button' type='button'
title='Previous Page(s)' data-tooltip-bottom='Previous Page(s)'
onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)} onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)}
disabled={visiblePages.includes(1)} disabled={visiblePages.includes(1)}
> >
@@ -231,7 +229,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='text-input' className='text-input'
type='text' type='text'
name='page' name='page'
title='Current page(s) in view' data-tooltip-bottom='Current page(s) in view'
inputMode='numeric' inputMode='numeric'
pattern='[0-9]' pattern='[0-9]'
value={pageNum} value={pageNum}
@@ -241,14 +239,14 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)} onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
style={{ width: `${pageNum.length}ch` }} style={{ width: `${pageNum.length}ch` }}
/> />
<span id='page-count' title='Total Page Count'>/ {totalPages}</span> <span id='page-count' data-tooltip-bottom='Total Page Count'>/ {totalPages}</span>
</div> </div>
<button <button
id='next-page' id='next-page'
className='tool' className='tool'
type='button' type='button'
title='Next Page(s)' data-tooltip-bottom='Next Page(s)'
onClick={()=>scrollToPage(_.max(visiblePages) + 1)} onClick={()=>scrollToPage(_.max(visiblePages) + 1)}
disabled={visiblePages.includes(totalPages)} disabled={visiblePages.includes(totalPages)}
> >
@@ -259,4 +257,4 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
); );
}; };
module.exports = ToolBar; export default ToolBar;
@@ -166,7 +166,7 @@
&.hidden { &.hidden {
flex-wrap : nowrap; flex-wrap : nowrap;
width : 92px; width : 50%;
overflow : hidden; overflow : hidden;
background-color : unset; background-color : unset;
opacity : 0.7; opacity : 0.7;
+54 -49
View File
@@ -1,14 +1,14 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
require('./editor.less'); import './editor.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash'); import _ from 'lodash';
const dedent = require('dedent-tabs').default; import dedent from 'dedent';
import Markdown from '../../../shared/markdown.js'; import Markdown from '@shared/markdown.js';
const CodeEditor = require('client/components/codeEditor/codeEditor.jsx'); import CodeEditor from '../../components/codeEditor/codeEditor.jsx';
const SnippetBar = require('./snippetbar/snippetbar.jsx'); import SnippetBar from './snippetbar/snippetbar.jsx';
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); import MetadataEditor from './metadataEditor/metadataEditor.jsx';
const EDITOR_THEME_KEY = 'HB_editor_theme'; const EDITOR_THEME_KEY = 'HB_editor_theme';
@@ -31,7 +31,7 @@ const DEFAULT_SNIPPET_TEXT = dedent`
`; `;
let isJumping = false; let isJumping = false;
const Editor = createClass({ const Editor = createReactClass({
displayName : 'Editor', displayName : 'Editor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -76,8 +76,8 @@ const Editor = createClass({
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('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200)); 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) {
@@ -86,9 +86,9 @@ const Editor = createClass({
}); });
} }
const snippetBar = document.querySelector('.editor > .snippetBar'); const snippetBar = document.querySelector('.editor > .snippetBar');
if (!snippetBar) return; if(!snippetBar) return;
this.resizeObserver = new ResizeObserver(entries => { this.resizeObserver = new ResizeObserver((entries)=>{
const height = document.querySelector('.editor > .snippetBar').offsetHeight; const height = document.querySelector('.editor > .snippetBar').offsetHeight;
this.setState({ snippetBarHeight: height }); this.setState({ snippetBarHeight: height });
}); });
@@ -117,7 +117,7 @@ const Editor = createClass({
}, },
componentWillUnmount() { componentWillUnmount() {
if (this.resizeObserver) this.resizeObserver.disconnect(); if(this.resizeObserver) this.resizeObserver.disconnect();
}, },
handleControlKeys : function(e){ handleControlKeys : function(e){
@@ -156,21 +156,21 @@ const Editor = createClass({
this.setState({ this.setState({
view : newView view : newView
}, ()=>{ }, ()=>{
this.codeEditor.current?.codeMirror.focus(); this.codeEditor.current?.codeMirror?.focus();
}); });
}, },
highlightCustomMarkdown : function(){ highlightCustomMarkdown : function(){
if(!this.codeEditor.current) return; if(!this.codeEditor.current?.codeMirror) return;
if((this.state.view === 'text') ||(this.state.view === 'snippet')) { if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
const codeMirror = this.codeEditor.current.codeMirror; const codeMirror = this.codeEditor.current.codeMirror;
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();
@@ -191,10 +191,10 @@ const Editor = createClass({
const textOrSnip = this.state.view === 'text'; const textOrSnip = this.state.view === 'text';
//reset custom line styles //reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine'); codeMirror?.removeLineClass(lineNumber, 'background', 'pageLine');
codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine'); codeMirror?.removeLineClass(lineNumber, 'background', 'snippetLine');
codeMirror.removeLineClass(lineNumber, 'text'); codeMirror?.removeLineClass(lineNumber, 'text');
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash'); codeMirror?.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
// 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
@@ -210,19 +210,19 @@ const Editor = createClass({
else if(this.state.view !== 'text') userSnippetCount += 1; else if(this.state.view !== 'text') userSnippetCount += 1;
// add back the original class 'background' but also add the new class '.pageline' // add back the original class 'background' but also add the new class '.pageline'
codeMirror.addLineClass(lineNumber, 'background', tabHighlight); codeMirror?.addLineClass(lineNumber, 'background', tabHighlight);
const pageCountElement = Object.assign(document.createElement('span'), { const pageCountElement = Object.assign(document.createElement('span'), {
className : 'editor-page-count', className : 'editor-page-count',
textContent : textOrSnip ? editorPageCount : userSnippetCount textContent : textOrSnip ? editorPageCount : userSnippetCount
}); });
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement); codeMirror?.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
}; };
// New Codemirror styling for V3 renderer // New CodeMirror styling for V3 renderer
if(this.props.renderer === 'V3') { if(this.props.renderer === 'V3') {
if(line.match(/^\\column(?:break)?$/)){ if(line.match(/^\\column(?:break)?$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit'); codeMirror?.addLineClass(lineNumber, 'text', 'columnSplit');
} }
// definition lists // definition lists
@@ -231,14 +231,14 @@ const Editor = createClass({
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){
codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' }); codeMirror?.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-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[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];
const colons = /::/g; const colons = /::/g;
const 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' });
} }
} }
} }
@@ -255,7 +255,7 @@ const Editor = createClass({
const 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' });
} }
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex)); startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
} }
@@ -266,7 +266,7 @@ const Editor = createClass({
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm; const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
let match; let match;
while ((match = regex.exec(line)) != null) { while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' }); codeMirror?.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
} }
} }
// Highlight inline spans {{content}} // Highlight inline spans {{content}}
@@ -284,7 +284,7 @@ const Editor = createClass({
blockCount = 0; blockCount = 0;
continue; continue;
} }
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' }); codeMirror?.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
} }
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){ } else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}} // Highlight block divs {{\n Content \n}}
@@ -293,7 +293,7 @@ const Editor = createClass({
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/); const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1 *$|^ *}}$/);
if(match) if(match)
endCh = match.index+match[0].length; endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' }); codeMirror?.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
} }
// Emojis // Emojis
@@ -314,11 +314,11 @@ const Editor = createClass({
const 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
const 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();
}); });
codeMirror.markText(startPos, endPos, { className: 'emoji' }); codeMirror?.markText(startPos, endPos, { className: 'emoji' });
} }
startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex)); startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
} }
@@ -337,7 +337,7 @@ const Editor = createClass({
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
@@ -378,44 +378,46 @@ const Editor = createClass({
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit); 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; const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top; let currentY = this.codeEditor.current.codeMirror?.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); let targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true);
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{ scrollingTimeout = setTimeout(()=>{
isJumping = false; isJumping = false;
this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete); this.codeEditor.current.codeMirror?.off('scroll', checkIfScrollComplete);
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done }, 150); // If 150 ms pass without a scroll event, assume scrolling is done
}; };
isJumping = true; isJumping = true;
checkIfScrollComplete(); checkIfScrollComplete();
this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete); if(this.codeEditor.current?.codeMirror) {
this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete);
}
if(smooth) { if(smooth) {
//Scroll 1/10 of the way every 10ms until 1px off. //Scroll 1/10 of the way every 10ms until 1px off.
const incrementalScroll = setInterval(()=>{ const incrementalScroll = setInterval(()=>{
currentY += (targetY - currentY) / 10; currentY += (targetY - currentY) / 10;
this.codeEditor.current.codeMirror.scrollTo(null, currentY); this.codeEditor.current.codeMirror?.scrollTo(null, currentY);
// Update target: target height is not accurate until within +-10 lines of the visible window // Update target: target height is not accurate until within +-10 lines of the visible window
if(Math.abs(targetY - currentY > 100)) if(Math.abs(targetY - currentY > 100))
targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true); targetY = this.codeEditor.current.codeMirror?.heightAtLine(targetLine, 'local', true);
// End when close enough // End when close enough
if(Math.abs(targetY - currentY) < 1) { if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
clearInterval(incrementalScroll); clearInterval(incrementalScroll);
} }
}, 10); }, 10);
} else { } else {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference this.codeEditor.current.codeMirror?.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 }); this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash'); this.codeEditor.current.codeMirror?.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
} }
}, },
@@ -440,6 +442,7 @@ const Editor = createClass({
<CodeEditor key='codeEditor' <CodeEditor key='codeEditor'
ref={this.codeEditor} ref={this.codeEditor}
language='gfm' language='gfm'
tab='brewText'
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onBrewChange('text')} onChange={this.props.onBrewChange('text')}
@@ -453,6 +456,7 @@ const Editor = createClass({
<CodeEditor key='codeEditor' <CodeEditor key='codeEditor'
ref={this.codeEditor} ref={this.codeEditor}
language='css' language='css'
tab='brewStyles'
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange('style')} onChange={this.props.onBrewChange('style')}
@@ -482,6 +486,7 @@ const Editor = createClass({
<CodeEditor key='codeEditor' <CodeEditor key='codeEditor'
ref={this.codeEditor} ref={this.codeEditor}
language='gfm' language='gfm'
tab='brewSnippets'
view={this.state.view} view={this.state.view}
value={this.props.brew.snippets} value={this.props.brew.snippets}
onChange={this.props.onBrewChange('snippets')} onChange={this.props.onBrewChange('snippets')}
@@ -542,4 +547,4 @@ const Editor = createClass({
} }
}); });
module.exports = Editor; export default Editor;
+4 -1
View File
@@ -1,9 +1,12 @@
@import 'themes/codeMirror/customEditorStyles.less'; @import '@sharedStyles/core.less';
@import '@themes/codeMirror/customEditorStyles.less';
.editor { .editor {
position : relative; position : relative;
width : 100%; width : 100%;
height : 100%; height : 100%;
container : editor / inline-size; container : editor / inline-size;
background:white;
.codeEditor { .codeEditor {
height : calc(100% - 25px); height : calc(100% - 25px);
.CodeMirror { height : 100%; } .CodeMirror { height : 100%; }
@@ -1,19 +1,17 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
require('./metadataEditor.less'); import './metadataEditor.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash'); import _ from 'lodash';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
const Combobox = require('client/components/combobox.jsx'); import Combobox from '../../../components/combobox.jsx';
const TagInput = require('../tagInput/tagInput.jsx'); import TagInput from '../tagInput/tagInput.jsx';
const Themes = require('themes/themes.json'); import Themes from '@themes/themes.json';
const validations = require('./validations.js'); import validations from './validations.js';
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']; import homebreweryThumbnail from '../../thumbnail.png';
const homebreweryThumbnail = require('../../thumbnail.png');
const callIfExists = (val, fn, ...args)=>{ const callIfExists = (val, fn, ...args)=>{
if(val[fn]) { if(val[fn]) {
@@ -21,7 +19,7 @@ const callIfExists = (val, fn, ...args)=>{
} }
}; };
const MetadataEditor = createClass({ const MetadataEditor = createReactClass({
displayName : 'MetadataEditor', displayName : 'MetadataEditor',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -34,7 +32,6 @@ const MetadataEditor = createClass({
tags : [], tags : [],
published : false, published : false,
authors : [], authors : [],
systems : [],
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB', theme : '5ePHB',
lang : 'en' lang : 'en'
@@ -92,15 +89,6 @@ const MetadataEditor = createClass({
} }
}, },
handleSystem : function(system, e){
if(e.target.checked){
this.props.metadata.systems.push(system);
} else {
this.props.metadata.systems = _.without(this.props.metadata.systems, system);
}
this.props.onChange(this.props.metadata);
},
handleRenderer : function(renderer, e){ handleRenderer : function(renderer, e){
if(e.target.checked){ if(e.target.checked){
this.props.metadata.renderer = renderer; this.props.metadata.renderer = renderer;
@@ -156,18 +144,6 @@ const MetadataEditor = createClass({
}); });
}, },
renderSystems : function(){
return _.map(SYSTEMS, (val)=>{
return <label key={val}>
<input
type='checkbox'
checked={_.includes(this.props.metadata.systems, val)}
onChange={(e)=>this.handleSystem(val, e)} />
{val}
</label>;
});
},
renderPublish : function(){ renderPublish : function(){
if(this.props.metadata.published){ if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}> return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
@@ -238,7 +214,7 @@ const MetadataEditor = createClass({
</div>; </div>;
} else { } else {
dropdown = dropdown =
<div className='value'> <div className='value' data-tooltip-top='Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.'>
<Combobox trigger='click' <Combobox trigger='click'
className='themes-dropdown' className='themes-dropdown'
default={currentThemeDisplay} default={currentThemeDisplay}
@@ -256,7 +232,6 @@ const MetadataEditor = createClass({
filterOn : ['value', 'title'] filterOn : ['value', 'title']
}} }}
/> />
<small>Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.</small>
</div>; </div>;
} }
@@ -281,7 +256,7 @@ const MetadataEditor = createClass({
return <div className='field language'> return <div className='field language'>
<label>language</label> <label>language</label>
<div className='value'> <div className='value' data-tooltip-right='Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.'>
<Combobox trigger='click' <Combobox trigger='click'
className='language-dropdown' className='language-dropdown'
default={this.props.metadata.lang || ''} default={this.props.metadata.lang || ''}
@@ -298,14 +273,13 @@ const MetadataEditor = createClass({
filterOn : ['value', 'detail', 'title'] filterOn : ['value', 'detail', 'title']
}} }}
/> />
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
</div> </div>
</div>; </div>;
}, },
renderRenderOptions : function(){ renderRenderOptions : function(){
return <div className='field systems'> return <div className='field renderers'>
<label>Renderer</label> <label>Renderer</label>
<div className='value'> <div className='value'>
<label key='legacy'> <label key='legacy'>
@@ -364,19 +338,21 @@ const MetadataEditor = createClass({
{this.renderThumbnail()} {this.renderThumbnail()}
</div> </div>
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]} <div className='field tags'>
placeholder='add tag' unique={true} <label>Tags</label>
values={this.props.metadata.tags} <div className='value' >
onChange={(e)=>this.handleFieldChange('tags', e)} <TagInput
/> label='tags'
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
<div className='field systems'> placeholder='add tag' unique={true}
<label>systems</label> values={this.props.metadata.tags}
<div className='value'> onChange={(e)=>this.handleFieldChange('tags', e)}
{this.renderSystems()} tooltip='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
/>
</div> </div>
</div> </div>
{this.renderLanguageDropdown()} {this.renderLanguageDropdown()}
{this.renderThemeDropdown()} {this.renderThemeDropdown()}
@@ -387,13 +363,22 @@ const MetadataEditor = createClass({
{this.renderAuthors()} {this.renderAuthors()}
<TagInput label='invited authors' valuePatterns={[/.+/]} <div className='field invitedAuthors'>
validators={[(v)=>!this.props.metadata.authors?.includes(v)]} <label>Invited authors</label>
placeholder='invite author' unique={true} <div className='value'>
values={this.props.metadata.invitedAuthors} <TagInput
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} label='invited authors'
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} valuePatterns={/.+/}
/> validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true}
tooltip={`Invited author usernames are case sensitive.
After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.`}
values={this.props.metadata.invitedAuthors}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
/>
</div>
</div>
<h2>Privacy</h2> <h2>Privacy</h2>
@@ -411,4 +396,4 @@ const MetadataEditor = createClass({
} }
}); });
module.exports = MetadataEditor; export default MetadataEditor;
@@ -1,4 +1,4 @@
@import 'naturalcrit/styles/colors.less'; @import '@sharedStyles/core.less';
.userThemeName { .userThemeName {
padding-right : 10px; padding-right : 10px;
@@ -44,8 +44,6 @@
gap : 10px; gap : 10px;
} }
.field { .field {
position : relative; position : relative;
display : flex; display : flex;
@@ -62,6 +60,7 @@
& > .value { & > .value {
flex : 1 1 auto; flex : 1 1 auto;
width : 50px; width : 50px;
&[data-tooltip-right] { max-width : 380px; }
&:invalid { background : #FFB9B9; } &:invalid { background : #FFB9B9; }
small { small {
display : block; display : block;
@@ -74,6 +73,16 @@
border : 1px solid gray; border : 1px solid gray;
&:focus { outline : 1px solid #444444; } &:focus { outline : 1px solid #444444; }
} }
&.description {
flex : 1;
textarea.value {
height : auto;
font-family : 'Open Sans', sans-serif;
resize : none;
}
}
&.thumbnail, &.themes { &.thumbnail, &.themes {
label { line-height : 2.0em; } label { line-height : 2.0em; }
.value { .value {
@@ -90,6 +99,15 @@
} }
} }
&.tags .tagInput-dropdown {
z-index : 400;
max-width : 200px;
}
&.language .value {
z-index : 300;
max-width : 150px;
}
&.themes { &.themes {
.value { .value {
overflow : visible; overflow : visible;
@@ -101,22 +119,13 @@
} }
} }
&.description { &.invitedAuthors .value {
flex : 1; z-index : 100;
textarea.value {
height : auto; .tagInput-dropdown { max-width : 200px; }
font-family : 'Open Sans', sans-serif;
resize : none;
}
}
&.language .language-dropdown {
z-index : 200;
max-width : 150px;
} }
} }
.thumbnail-preview { .thumbnail-preview {
position : relative; position : relative;
flex : 1 1; flex : 1 1;
@@ -129,7 +138,7 @@
background-color : #AAAAAA; background-color : #AAAAAA;
} }
.systems.field .value { .renderers.field .value {
label { label {
display : inline-flex; display : inline-flex;
align-items : center; align-items : center;
@@ -169,7 +178,7 @@
.themes.field { .themes.field {
& .dropdown-container { & .dropdown-container {
position : relative; position : relative;
z-index : 100; z-index : 200;
background-color : white; background-color : white;
} }
& .dropdown-options { overflow-y : visible; } & .dropdown-options { overflow-y : visible; }
@@ -1,4 +1,4 @@
module.exports = { export default {
title : [ title : [
(value)=>{ (value)=>{
return value?.length > 100 ? 'Max title length of 100 characters' : null; return value?.length > 100 ? 'Max title length of 100 characters' : null;
@@ -1,29 +1,36 @@
/*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/
require('./snippetbar.less'); import './snippetbar.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash');
const cx = require('classnames'); import _ from 'lodash';
import cx from 'classnames';
import { loadHistory } from '../../utils/versionHistory.js'; import { loadHistory } from '../../utils/versionHistory.js';
import { brewSnippetsToJSON } from '../../../../shared/helpers.js'; import { brewSnippetsToJSON } from '@shared/helpers.js';
//Import all themes import Legacy5ePHB from '@themes/Legacy/5ePHB/snippets.js';
const ThemeSnippets = {}; import V3_5ePHB from '@themes/V3/5ePHB/snippets.js';
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js'); import V3_5eDMG from '@themes/V3/5eDMG/snippets.js';
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js'); import V3_Journal from '@themes/V3/Journal/snippets.js';
ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js'); import V3_Blank from '@themes/V3/Blank/snippets.js';
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json'); const ThemeSnippets = {
Legacy_5ePHB : Legacy5ePHB,
V3_5ePHB : V3_5ePHB,
V3_5eDMG : V3_5eDMG,
V3_Journal : V3_Journal,
V3_Blank : V3_Blank,
};
import EditorThemes from '../../../../build/homebrew/codeMirror/editorThemes.json';
const execute = function(val, props){ const execute = function(val, props){
if(_.isFunction(val)) return val(props); if(_.isFunction(val)) return val(props);
return val; return val;
}; };
const Snippetbar = createClass({ const Snippetbar = createReactClass({
displayName : 'SnippetBar', displayName : 'SnippetBar',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -281,9 +288,9 @@ const Snippetbar = createClass({
} }
}); });
module.exports = Snippetbar; export default Snippetbar;
const SnippetGroup = createClass({ const SnippetGroup = createReactClass({
displayName : 'SnippetGroup', displayName : 'SnippetGroup',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -1,5 +1,6 @@
@import '@sharedStyles/core.less';
@import (less) './client/icons/customIcons.less'; @import (less) './client/icons/customIcons.less';
@import (less) '././././themes/fonts/5e/fonts.less'; @import (less) '@themes/fonts/5e/fonts.less';
.snippetBar { .snippetBar {
@menuHeight : 25px; @menuHeight : 25px;
@@ -0,0 +1,219 @@
export const tagSuggestionList = [
// ############################## Systems
// D&D
'system:D&D Original',
'system:D&D Basic',
'system:AD&D 1e',
'system:AD&D 2e',
'system:D&D 3e',
'system:D&D 3.5e',
'system:D&D 4e',
'system:D&D 5e',
'system:D&D 5e 2024',
'system:BD&D (B/X)',
'system:D&D Essentials',
// Other Famous RPGs
'system:Pathfinder 1e',
'system:Pathfinder 2e',
'system:Vampire: The Masquerade',
'system:Werewolf: The Apocalypse',
'system:Mage: The Ascension',
'system:Call of Cthulhu',
'system:Shadowrun',
'system:Star Wars RPG (D6/D20/Edge of the Empire)',
'system:Warhammer Fantasy Roleplay',
'system:Cyberpunk 2020',
'system:Blades in the Dark',
'system:Daggerheart',
'system:Draw Steel',
'system:Mutants and Masterminds',
// Meta
'meta:V3',
'meta:Legacy',
'meta:Template',
'meta:Theme',
'meta:free',
'meta:Character Sheet',
'meta:Documentation',
'meta:NPC',
'meta:Guide',
'meta:Resource',
'meta:Notes',
'meta:Example',
// Book type
'type:Campaign',
'type:Campaign Setting',
'type:Adventure',
'type:One-Shot',
'type:Setting',
'type:World',
'type:Lore',
'type:History',
'type:Dungeon Master',
'type:Encounter Pack',
'type:Encounter',
'type:Session Notes',
'type:reference',
'type:Handbook',
'type:Manual',
'type:Manuals',
'type:Compendium',
'type:Bestiary',
// ###################################### RPG Keywords
// Classes / Subclasses / Archetypes
'Class',
'Subclass',
'Archetype',
'Martial',
'Half-Caster',
'Full Caster',
'Artificer',
'Barbarian',
'Bard',
'Cleric',
'Druid',
'Fighter',
'Monk',
'Paladin',
'Rogue',
'Sorcerer',
'Warlock',
'Wizard',
// Races / Species / Lineages
'Race',
'Ancestry',
'Lineage',
'Aasimar',
'Beastfolk',
'Dragonborn',
'Dwarf',
'Elf',
'Goblin',
'Half-Elf',
'Half-Orc',
'Human',
'Kobold',
'Lizardfolk',
'Lycan',
'Orc',
'Tiefling',
'Vampire',
'Yuan-Ti',
// Magic / Spells / Items
'Magic',
'Magic Item',
'Magic Items',
'Wondrous Item',
'Magic Weapon',
'Artifact',
'Spell',
'Spells',
'Cantrip',
'Cantrips',
'Eldritch',
'Eldritch Invocation',
'Invocation',
'Invocations',
'Pact boon',
'Pact Boon',
'Spellcaster',
'Spellblade',
'Magical Tattoos',
'Enchantment',
'Enchanted',
'Attunement',
'Requires Attunement',
'Rune',
'Runes',
'Wand',
'Rod',
'Scroll',
'Potion',
'Potions',
'Item',
'Items',
'Bag of Holding',
// Monsters / Creatures / Enemies
'Monster',
'Creatures',
'Creature',
'Beast',
'Beasts',
'Humanoid',
'Undead',
'Fiend',
'Aberration',
'Ooze',
'Giant',
'Dragon',
'Monstrosity',
'Demon',
'Devil',
'Elemental',
'Construct',
'Constructs',
'Boss',
'BBEG',
// ############################# Media / Pop Culture
'One Piece',
'Dragon Ball',
'Dragon Ball Z',
'Naruto',
'Jujutsu Kaisen',
'Fairy Tail',
'Final Fantasy',
'Kingdom Hearts',
'Elder Scrolls',
'Skyrim',
'WoW',
'World of Warcraft',
'Marvel Comics',
'DC Comics',
'Pokemon',
'League of Legends',
'Runeterra',
'Arcane',
'Yu-Gi-Oh',
'Minecraft',
'Don\'t Starve',
'Witcher',
'Witcher 3',
'Cyberpunk',
'Cyberpunk 2077',
'Fallout',
'Divinity Original Sin 2',
'Fullmetal Alchemist',
'Fullmetal Alchemist Brotherhood',
'Lobotomy Corporation',
'Bloodborne',
'Dragonlance',
'Shackled City Adventure Path',
'Baldurs Gate 3',
'Library of Ruina',
'Radiant Citadel',
'Ravenloft',
'Forgotten Realms',
'Exandria',
'Critical Role',
'Star Wars',
'SW5e',
'Star Wars 5e',
];
// substrings to be normalized to the first value on the array
export const canonizationList = [
['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'],
['5e', '5th Edition'],
['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'],
['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'],
['P2e', 'p2e', 'P2E', 'Pathfinder 2e'],
];
+171 -78
View File
@@ -1,105 +1,198 @@
require('./tagInput.less'); import './tagInput.less';
const React = require('react'); import React, { useState, useEffect } from 'react';
const { useState, useEffect } = React; import Combobox from '../../../components/combobox.jsx';
const _ = require('lodash');
const TagInput = ({ unique = true, values = [], ...props })=>{ import { tagSuggestionList, canonizationList } from './curatedTagSuggestionList.js';
const [tempInputText, setTempInputText] = useState('');
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false }))); const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
const [tagList, setTagList] = useState(
values.map((value)=>({
value,
editing : false,
draft : '',
})),
);
useEffect(()=>{ useEffect(()=>{
handleChange(tagList.map((context)=>context.value)); const incoming = values || [];
const current = tagList.map((t)=>t.value);
const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]);
if(changed) {
setTagList(
incoming.map((value)=>({
value,
editing : false,
})),
);
}
}, [values]);
useEffect(()=>{
onChange?.({
target : { value: tagList.map((t)=>t.value) },
});
}, [tagList]); }, [tagList]);
const handleChange = (value)=>{ const normalizeValue = (input)=>{
props.onChange({ const lowerInput = input.toLowerCase();
target : { value } let normalizedTag = input;
});
};
const handleInputKeyDown = ({ evt, value, index, options = {} })=>{ for (const group of canonizationList) {
if(_.includes(['Enter', ','], evt.key)) { for (const tag of group) {
evt.preventDefault(); if(!tag) continue;
submitTag(evt.target.value, value, index);
if(options.clear) { const index = lowerInput.indexOf(tag.toLowerCase());
setTempInputText(''); if(index !== -1) {
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
break;
}
} }
} }
if(normalizedTag.includes(':')) {
const [rawType, rawValue = ''] = normalizedTag.split(':');
const tagType = rawType.trim().toLowerCase();
const tagValue = rawValue.trim();
if(tagValue.length > 0) {
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
}
//trims spaces around colon and capitalizes the first word after the colon
//this is preferred to users not understanding they can't put spaces in
}
return normalizedTag;
}; };
const submitTag = (newValue, originalValue, index)=>{ const submitTag = (newValue, index = null)=>{
setTagList((prevContext)=>{ const trimmed = newValue?.trim();
// remove existing tag if(!trimmed) return;
if(newValue === null){ if(!valuePatterns.test(trimmed)) return;
return [...prevContext].filter((context, i)=>i !== index);
const normalizedTag = normalizeValue(trimmed);
setTagList((prev)=>{
const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase());
if(unique && existsIndex !== -1) return prev;
if(index !== null) {
return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t));
} }
// add new tag
if(originalValue === null){ return [...prev, { value: normalizedTag, editing: false }];
return [...prevContext, { value: newValue, editing: false }];
}
// update existing tag
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, value: newValue, editing: false };
}
return context;
});
}); });
}; };
const removeTag = (index)=>{
setTagList((prev)=>prev.filter((_, i)=>i !== index));
};
const editTag = (index)=>{ const editTag = (index)=>{
setTagList((prevContext)=>{ setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t)));
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, editing: true };
}
return { ...context, editing: false };
});
});
}; };
const renderReadTag = (context, index)=>{ const stopEditing = (index)=>{
return ( setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t)));
<li key={index}
data-value={context.value}
className='tag'
onClick={()=>editTag(index)}>
{context.value}
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
</li>
);
}; };
const renderWriteTag = (context, index)=>{ const suggestionOptions = tagSuggestionList.map((tag)=>{
const tagType = tag.split(':');
let classes = 'item';
switch (tagType[0]) {
case 'type':
classes = 'item type';
break;
case 'group':
classes = 'item group';
break;
case 'meta':
classes = 'item meta';
break;
case 'system':
classes = 'item system';
break;
default:
classes = 'item';
break;
}
return ( return (
<input type='text' <div className={classes} key={`tag-${tag}`} value={tag} data={tag}>
key={index} {tag}
defaultValue={context.value} </div>
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
autoFocus
/>
); );
}; });
return ( return (
<div className='field'> <div className='tagInputWrap'>
<label>{props.label}</label> <Combobox
<div className='value'> trigger='click'
<ul className='list'> className='tagInput-dropdown'
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} default=''
</ul> placeholder={placeholder}
options={label === 'tags' ? suggestionOptions : []}
<input tooltip={tooltip}
type='text' autoSuggest={
className='value' label === 'tags'
placeholder={props.placeholder} ? {
value={tempInputText} suggestMethod : 'startsWith',
onChange={(e)=>setTempInputText(e.target.value)} clearAutoSuggestOnClick : true,
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })} filterOn : ['value', 'title'],
/> }
</div> : { suggestMethod: 'includes', clearAutoSuggestOnClick: true, filterOn: [] }
}
valuePatterns={valuePatterns.source}
onSelect={(value)=>submitTag(value)}
onEntry={(e)=>{
if(e.key === 'Enter') {
e.preventDefault();
submitTag(e.target.value);
}
}}
/>
<ul className='list'>
{tagList.map((t, i)=>t.editing ? (
<input
key={i}
type='text'
value={t.draft} // always use draft
pattern={valuePatterns.source}
onChange={(e)=>setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)),
)
}
onKeyDown={(e)=>{
if(e.key === 'Enter') {
e.preventDefault();
submitTag(t.draft, i); // submit draft
setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)),
);
}
if(e.key === 'Escape') {
stopEditing(i);
e.target.blur();
}
}}
autoFocus
/>
) : (
<li key={i} className='tag' onClick={()=>editTag(i)}>
{t.value}
<button
type='button'
onClick={(e)=>{
e.stopPropagation();
removeTag(i);
}}>
<i className='fa fa-times fa-fw' />
</button>
</li>
),
)}
</ul>
</div> </div>
); );
}; };
module.exports = TagInput; export default TagInput;
@@ -0,0 +1,31 @@
.tags {
.tagInputWrap {
display:grid;
grid-template-columns: 200px 3fr;
gap:10px;
}
.list input {
border-radius: 5px;
}
.tagInput-dropdown {
.dropdown-options {
.item {
&.type {
background-color: #00800035;
}
&.group {
background-color: #50505035;
}
&.meta {
background-color: #00008035;
}
&.system {
background-color: #80000035;
}
}
}
}
}
File diff suppressed because it is too large Load Diff
+24 -15
View File
@@ -1,8 +1,7 @@
/* eslint-disable camelcase */ import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
import './homebrew.less'; import './homebrew.less';
import React from 'react'; import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js'; import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
@@ -41,24 +40,34 @@ const Homebrew = (props)=>{
brews brews
} = props; } = props;
global.account = account;
global.version = version;
global.config = config;
const backgroundObject = ()=>{ const backgroundObject = ()=>{
if(global.config.deployment || (config.local && config.development)){ if(config?.deployment || (config?.local && config?.development)) {
const bgText = global.config.deployment || 'Local'; const bgText = config?.deployment || 'Local';
return { return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")` backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
}; };
} }
return null; return null;
}; };
updateLocalStorage(); updateLocalStorage();
if(brew.pureError) {
return (
<Router>
<div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes>
<Route path={brew.originalUrl} element={<WithRoute el={ErrorPage} brew={brew} />} />
</Routes>
</div>
</Router>
);
}
return ( return (
<Router location={url}> <Router>
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}> <div className={`homebrew${(config?.deployment || config?.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes> <Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} /> <Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} /> <Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
@@ -80,4 +89,4 @@ const Homebrew = (props)=>{
); );
}; };
module.exports = Homebrew; export default Homebrew;
+1 -1
View File
@@ -1,4 +1,4 @@
@import 'naturalcrit/styles/core.less'; @import '@sharedStyles/core.less';
.homebrew { .homebrew {
height : 100%; height : 100%;
background-color:@steel; background-color:@steel;
+6
View File
@@ -0,0 +1,6 @@
import { createRoot } from 'react-dom/client';
import Homebrew from './homebrew.jsx';
const props = window.__INITIAL_PROPS__ || {};
createRoot(document.getElementById('reactRoot')).render(<Homebrew {...props} />);
+7 -7
View File
@@ -1,9 +1,9 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const Nav = require('client/homebrew/navbar/nav.jsx'); import request from 'superagent';
const request = require('superagent'); import Nav from './nav.jsx';
const Account = createClass({ const Account = createReactClass({
displayName : 'AccountNavItem', displayName : 'AccountNavItem',
getInitialState : function() { getInitialState : function() {
return { return {
@@ -97,7 +97,7 @@ const Account = createClass({
// Logged out // Logged out
// LOCAL ONLY // LOCAL ONLY
if(global.config.local) { if(global.config?.local) {
return <Nav.item color='teal' icon='fas fa-sign-in-alt' onClick={this.localLogin}> return <Nav.item color='teal' icon='fas fa-sign-in-alt' onClick={this.localLogin}>
login login
</Nav.item>; </Nav.item>;
@@ -111,4 +111,4 @@ const Account = createClass({
} }
}); });
module.exports = Account; export default Account;
+4 -4
View File
@@ -1,6 +1,6 @@
require('./error-navitem.less'); import './error-navitem.less';
const React = require('react'); import React from 'react';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
const ErrorNavItem = ({ error = '', clearError })=>{ const ErrorNavItem = ({ error = '', clearError })=>{
const response = error.response; const response = error.response;
@@ -144,4 +144,4 @@ const ErrorNavItem = ({ error = '', clearError })=>{
</Nav.item>; </Nav.item>;
}; };
module.exports = ErrorNavItem; export default ErrorNavItem;
@@ -1,3 +1,5 @@
@import '@sharedStyles/core.less';
.navItem.error { .navItem.error {
position : relative; position : relative;
background-color : @red; background-color : @red;
+4 -4
View File
@@ -1,9 +1,9 @@
const React = require('react'); import React from 'react';
const dedent = require('dedent-tabs').default; import dedent from 'dedent';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
module.exports = function(props){ export default function(props){
return <Nav.dropdown> return <Nav.dropdown>
<Nav.item color='grey' icon='fas fa-question-circle'> <Nav.item color='grey' icon='fas fa-question-circle'>
need help? need help?
+6 -15
View File
@@ -1,11 +1,11 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const Moment = require('moment'); import Moment from 'moment';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
const MetadataNav = createClass({ const MetadataNav = createReactClass({
displayName : 'MetadataNav', displayName : 'MetadataNav',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -46,11 +46,6 @@ const MetadataNav = createClass({
</>; </>;
}, },
getSystems : function(){
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
return this.props.brew.systems.join(', ');
},
renderMetaWindow : function(){ renderMetaWindow : function(){
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}> return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
<div className='row'> <div className='row'>
@@ -65,10 +60,6 @@ const MetadataNav = createClass({
<h4>Tags</h4> <h4>Tags</h4>
<p>{this.getTags()}</p> <p>{this.getTags()}</p>
</div> </div>
<div className='row'>
<h4>Systems</h4>
<p>{this.getSystems()}</p>
</div>
<div className='row'> <div className='row'>
<h4>Updated</h4> <h4>Updated</h4>
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p> <p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
@@ -86,4 +77,4 @@ const MetadataNav = createClass({
}); });
module.exports = MetadataNav; export default MetadataNav;
+10 -11
View File
@@ -1,14 +1,13 @@
require('client/homebrew/navbar/navbar.less'); import './navbar.less';
const React = require('react'); import React, { useState, useRef, useEffect } from 'react';
const { useState, useRef, useEffect } = React; import createReactClass from 'create-react-class';
const createClass = require('create-react-class'); import _ from 'lodash';
const _ = require('lodash'); import cx from 'classnames';
const cx = require('classnames');
const NaturalCritIcon = require('client/components/svg/naturalcrit-d20.svg.jsx'); import NaturalCritIcon from '../../components/svg/naturalcrit-d20.svg.jsx';
const Nav = { const Nav = {
base : createClass({ base : createReactClass({
displayName : 'Nav.base', displayName : 'Nav.base',
render : function(){ render : function(){
return <nav> return <nav>
@@ -25,7 +24,7 @@ const Nav = {
</a>; </a>;
}, },
section : createClass({ section : createReactClass({
displayName : 'Nav.section', displayName : 'Nav.section',
render : function(){ render : function(){
return <div className={`navSection ${this.props.className ?? ''}`}> return <div className={`navSection ${this.props.className ?? ''}`}>
@@ -34,7 +33,7 @@ const Nav = {
} }
}), }),
item : createClass({ item : createReactClass({
displayName : 'Nav.item', displayName : 'Nav.item',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -117,4 +116,4 @@ const Nav = {
}; };
module.exports = Nav; export default Nav;
+11 -17
View File
@@ -1,23 +1,17 @@
require('./navbar.less'); import './navbar.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
const PatreonNavItem = require('./patreon.navitem.jsx'); import PatreonNavItem from './patreon.navitem.jsx';
const Navbar = createClass({ const Navbar = createReactClass({
displayName : 'Navbar', displayName : 'Navbar',
getInitialState : function() { getInitialState : function() {
return { return {
//showNonChromeWarning : false, // showNonChromeWarning: false, // uncomment if needed
ver : '0.0.0' ver : global.version || '0.0.0'
}; };
},
getInitialState : function() {
return {
ver : global.version
};
}, },
/* /*
@@ -49,4 +43,4 @@ const Navbar = createClass({
} }
}); });
module.exports = Navbar; export default Navbar;
+1 -1
View File
@@ -1,4 +1,4 @@
@import 'naturalcrit/styles/colors.less'; @import '@sharedStyles/core.less';
@navbarHeight : 28px; @navbarHeight : 28px;
@viewerToolsHeight : 32px; @viewerToolsHeight : 32px;
+6 -6
View File
@@ -1,7 +1,7 @@
const React = require('react'); import React from 'react';
const _ = require('lodash'); import _ from 'lodash';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js import { splitTextStyleAndMetadata } from '@shared/helpers.js';
const BREWKEY = 'HB_newPage_content'; const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style'; const STYLEKEY = 'HB_newPage_style';
@@ -24,7 +24,7 @@ const NewBrew = ()=>{
localStorage.setItem(BREWKEY, newBrew.text); localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style); localStorage.setItem(STYLEKEY, newBrew.style);
localStorage.setItem(METAKEY, JSON.stringify( localStorage.setItem(METAKEY, JSON.stringify(
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']) _.pick(newBrew, ['title', 'description', 'tags', 'renderer', 'theme', 'lang'])
)); ));
window.location.href = '/new'; window.location.href = '/new';
return; return;
@@ -100,4 +100,4 @@ const NewBrew = ()=>{
); );
}; };
module.exports = NewBrew; export default NewBrew;
+3 -3
View File
@@ -1,7 +1,7 @@
const React = require('react'); import React from 'react';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
module.exports = function(props){ export default function(props){
return <Nav.item return <Nav.item
className='patreon' className='patreon'
newTab={true} newTab={true}
+4 -4
View File
@@ -1,8 +1,8 @@
const React = require('react'); import React from 'react';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
const { printCurrentBrew } = require('../../../shared/helpers.js'); import { printCurrentBrew } from '@shared/helpers.js';
module.exports = function(){ export default function(){
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'> return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
get PDF get PDF
</Nav.item>; </Nav.item>;
+7 -7
View File
@@ -1,15 +1,15 @@
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash'); import _ from 'lodash';
const Moment = require('moment'); import Moment from 'moment';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
const EDIT_KEY = 'HB_nav_recentlyEdited'; const EDIT_KEY = 'HB_nav_recentlyEdited';
const VIEW_KEY = 'HB_nav_recentlyViewed'; const VIEW_KEY = 'HB_nav_recentlyViewed';
const RecentItems = createClass({ const RecentItems = createReactClass({
DisplayName : 'RecentItems', DisplayName : 'RecentItems',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -175,7 +175,7 @@ const RecentItems = createClass({
}); });
module.exports = { export default {
edited : (props)=>{ edited : (props)=>{
return <RecentItems return <RecentItems
+2 -2
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import dedent from 'dedent-tabs'; import dedent from 'dedent';
import Nav from 'client/homebrew/navbar/nav.jsx'; import Nav from './nav.jsx';
const getShareId = (brew)=>( const getShareId = (brew)=>(
brew.googleId && !brew.stubbed brew.googleId && !brew.stubbed
+3 -3
View File
@@ -1,8 +1,8 @@
const React = require('react'); import React from 'react';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from './nav.jsx';
module.exports = function (props) { export default function (props) {
return ( return (
<Nav.item <Nav.item
color='purple' color='purple'
@@ -1,7 +1,7 @@
const React = require('react'); import React from 'react';
const moment = require('moment'); import moment from 'moment';
const UIPage = require('../basePages/uiPage/uiPage.jsx'); import UIPage from '../basePages/uiPage/uiPage.jsx';
const NaturalCritIcon = require('client/components/svg/naturalcrit-d20.svg.jsx'); import NaturalCritIcon from '../../../components/svg/naturalcrit-d20.svg.jsx';
let SAVEKEY = ''; let SAVEKEY = '';
@@ -79,4 +79,4 @@ const AccountPage = (props)=>{
</UIPage>); </UIPage>);
}; };
module.exports = AccountPage; export default AccountPage;
@@ -1,12 +1,11 @@
require('./brewItem.less'); import './brewItem.less';
const React = require('react'); import React, { useCallback } from 'react';
const { useCallback } = React; import moment from 'moment';
const moment = require('moment');
import request from '../../../../utils/request-middleware.js'; import request from '../../../../utils/request-middleware.js';
const googleDriveIcon = require('../../../../googleDrive.svg'); import googleDriveIcon from '../../../../googleDrive.svg';
const homebreweryIcon = require('../../../../thumbnail.svg'); import homebreweryIcon from '../../../../thumbnail.svg';
const dedent = require('dedent-tabs').default; import dedent from 'dedent';
const BrewItem = ({ const BrewItem = ({
brew = { brew = {
@@ -176,4 +175,4 @@ const BrewItem = ({
); );
}; };
module.exports = BrewItem; export default BrewItem;
@@ -1,3 +1,4 @@
@import '@sharedStyles/core.less';
.brewItem { .brewItem {
position : relative; position : relative;
@@ -1,11 +1,11 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./listPage.less'); import './listPage.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const _ = require('lodash'); import _ from 'lodash';
const moment = require('moment'); import moment from 'moment';
const BrewItem = require('./brewItem/brewItem.jsx'); import BrewItem from './brewItem/brewItem.jsx';
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir'; const USERPAGE_SORT_DIR = 'HB_listPage_sortDir';
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType'; const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
@@ -14,7 +14,7 @@ const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
const DEFAULT_SORT_TYPE = 'alpha'; const DEFAULT_SORT_TYPE = 'alpha';
const DEFAULT_SORT_DIR = 'asc'; const DEFAULT_SORT_DIR = 'asc';
const ListPage = createClass({ const ListPage = createReactClass({
displayName : 'ListPage', displayName : 'ListPage',
getDefaultProps : function() { getDefaultProps : function() {
return { return {
@@ -279,4 +279,4 @@ const ListPage = createClass({
} }
}); });
module.exports = ListPage; export default ListPage;
@@ -1,16 +1,17 @@
require('./uiPage.less'); import './uiPage.less';
const React = require('react'); import React from 'react';
const createClass = require('create-react-class'); import createReactClass from 'create-react-class';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from '../../../navbar/nav.jsx';
const Navbar = require('client/homebrew/navbar/navbar.jsx'); import Navbar from '../../../navbar/navbar.jsx';
const NewBrewItem = require('client/homebrew/navbar/newbrew.navitem.jsx'); import NewBrewItem from '../../../navbar/newbrew.navitem.jsx';
const HelpNavItem = require('client/homebrew/navbar/help.navitem.jsx'); import HelpNavItem from '../../../navbar/help.navitem.jsx';
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both; import RecentNavItems from '../../../navbar/recent.navitem.jsx';
const Account = require('client/homebrew/navbar/account.navitem.jsx'); const { both: RecentNavItem } = RecentNavItems;
import Account from '../../../navbar/account.navitem.jsx';
const UIPage = createClass({ const UIPage = createReactClass({
displayName : 'UIPage', displayName : 'UIPage',
render : function(){ render : function(){
@@ -35,4 +36,4 @@ const UIPage = createClass({
} }
}); });
module.exports = UIPage; export default UIPage;
+33 -31
View File
@@ -4,33 +4,35 @@ import './editPage.less';
// Common imports // Common imports
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'markdown.js'; import Markdown from '@shared/markdown.js';
import _ from 'lodash'; import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'client/homebrew/navbar/nav.jsx'; import Nav from '@navbar/nav.jsx';
import Navbar from 'client/homebrew/navbar/navbar.jsx'; import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from 'client/homebrew/navbar/newbrew.navitem.jsx'; import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from 'client/homebrew/navbar/account.navitem.jsx'; import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx'; import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx'; import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx'; import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from 'client/homebrew/navbar/print.navitem.jsx'; import PrintNavItem from '@navbar/print.navitem.jsx';
import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports // Page specific imports
import { Meta } from 'vitreum/headtags'; import Headtags from '../../../../vitreum/headtags.js';
const Meta = Headtags.Meta;
import { md5 } from 'hash-wasm'; import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate'; import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch'; import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import ShareNavItem from 'client/homebrew/navbar/share.navitem.jsx'; import ShareNavItem from '@navbar/share.navitem.jsx';
import LockNotification from './lockNotification/lockNotification.jsx'; import LockNotification from './lockNotification/lockNotification.jsx';
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg'; import googleDriveIcon from '../../googleDrive.svg';
@@ -55,28 +57,28 @@ const EditPage = (props)=>{
...props ...props
}; };
const [currentBrew , setCurrentBrew ] = useState(props.brew); const [currentBrew, setCurrentBrew] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [lastSavedTime , setLastSavedTime ] = useState(new Date()); const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); const [saveGoogle, setSaveGoogle] = useState(!!props.brew.googleId);
const [error , setError ] = useState(null); const [error, setError] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed); const [alertTrashedGoogleBrew, setAlertTrashedGoogleBrew] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); const [alertLoginToTransfer, setAlertLoginToTransfer] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); const [confirmGoogleTransfer, setConfirmGoogleTransfer] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const saveTimeout = useRef(null); const saveTimeout = useRef(null);
const warnUnsavedTimeout = useRef(null); const warnUnsavedTimeout = useRef(null);
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{ useEffect(()=>{
@@ -216,7 +218,7 @@ const EditPage = (props)=>{
text : brew.text.normalize('NFC'), text : brew.text.normalize('NFC'),
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
hash : await md5(lastSavedBrew.current.text), hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
textBin : undefined, textBin : undefined,
version : lastSavedBrew.current.version version : lastSavedBrew.current.version
}; };
@@ -415,4 +417,4 @@ const EditPage = (props)=>{
); );
}; };
module.exports = EditPage; export default EditPage;
@@ -40,4 +40,4 @@ function LockNotification(props) {
</Dialog>; </Dialog>;
}; };
module.exports = LockNotification; export default LockNotification;
@@ -1,8 +1,8 @@
require('./errorPage.less'); import './errorPage.less';
const React = require('react'); import React from 'react';
const UIPage = require('../basePages/uiPage/uiPage.jsx'); import UIPage from '../basePages/uiPage/uiPage.jsx';
import Markdown from '../../../../shared/markdown.js'; import Markdown from '@shared/markdown.js';
const ErrorIndex = require('./errors/errorIndex.js'); import ErrorIndex from './errors/errorIndex.js';
const ErrorPage = ({ brew })=>{ const ErrorPage = ({ brew })=>{
// Retrieving the error text based on the brew's error code from ErrorIndex // Retrieving the error text based on the brew's error code from ErrorIndex
@@ -22,4 +22,4 @@ const ErrorPage = ({ brew })=>{
); );
}; };
module.exports = ErrorPage; export default ErrorPage;
@@ -1,7 +1,6 @@
.homebrew { .homebrew {
.uiPage.sitePage { .uiPage.sitePage:has(.errorTitle) {
.errorTitle { .errorTitle {
//background-color: @orange;
color : #D02727; color : #D02727;
text-align : center; text-align : center;
} }
@@ -1,4 +1,4 @@
const dedent = require('dedent-tabs').default; import dedent from 'dedent';
const loginUrl = 'https://www.naturalcrit.com/login'; const loginUrl = 'https://www.naturalcrit.com/login';
@@ -268,4 +268,4 @@ const errorIndex = (props)=>{
}; };
}; };
module.exports = errorIndex; export default errorIndex;
+26 -23
View File
@@ -1,31 +1,34 @@
/* eslint-disable max-lines */
import './homePage.less'; import './homePage.less';
// Common imports // Common imports
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'markdown.js'; import Markdown from '@shared/markdown.js';
import _ from 'lodash'; import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'client/homebrew/navbar/nav.jsx'; import Nav from '@navbar/nav.jsx';
import Navbar from 'client/homebrew/navbar/navbar.jsx'; import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from 'client/homebrew/navbar/newbrew.navitem.jsx'; import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from 'client/homebrew/navbar/account.navitem.jsx'; import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx'; import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx'; import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx'; import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from 'client/homebrew/navbar/print.navitem.jsx'; import PrintNavItem from '@navbar/print.navitem.jsx';
import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports // Page specific imports
import { Meta } from 'vitreum/headtags'; import Headtags from '@vitreum/headtags.js';
const Meta = Headtags.Meta;
const BREWKEY = 'homebrewery-new'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
@@ -42,16 +45,16 @@ const HomePage =(props)=>{
...props ...props
}; };
const [currentBrew , setCurrentBrew] = useState(props.brew); const [currentBrew, setCurrentBrew] = useState(props.brew);
const [error , setError] = useState(undefined); const [error, setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text)); const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [isSaving , setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false); const [autoSaveEnabled, setAutoSaveEnable] = useState(false);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));
@@ -230,4 +233,4 @@ const HomePage =(props)=>{
); );
}; };
module.exports = HomePage; export default HomePage;
@@ -1,3 +1,5 @@
@import '@sharedStyles/core.less';
.homePage { .homePage {
position : relative; position : relative;
a.floatingNewButton { a.floatingNewButton {
@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew! If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
}} }}
![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px} ![homebrew mug](https://homebrewery.naturalcrit.com/assets/homebrewerymug.png) {position:absolute,bottom:20px,left:130px,width:220px}
{{artist,bottom:160px,left:100px {{artist,bottom:160px,left:100px
##### Homebrew Mug ##### Homebrew Mug
@@ -77,16 +77,16 @@ If you wish to sell or in some way gain profit for what's created on this site,
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery. If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
### More Homebrew Resources ### More Homebrew Resources
[![Discord](/assets/discordOfManyThings.svg){width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx) [![Discord](https://homebrewery.naturalcrit.com/assets/discordOfManyThings.svg){width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback. If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
{{position:absolute;top:20px;right:20px;width:auto {{position:absolute;top:20px;right:20px;width:auto
[![Discord](/assets/discord.png){height:30px}](https://discord.gg/by3deKx) [![Discord](https://homebrewery.naturalcrit.com/assets/discord.png){height:30px}](https://discord.gg/by3deKx)
[![Github](/assets/github.png){height:30px}](https://github.com/naturalcrit/homebrewery) [![Github](https://homebrewery.naturalcrit.com/assets/github.png){height:30px}](https://github.com/naturalcrit/homebrewery)
[![Patreon](/assets/patreon.png){height:30px}](https://patreon.com/NaturalCrit) [![Patreon](https://homebrewery.naturalcrit.com/assets/patreon.png){height:30px}](https://patreon.com/NaturalCrit)
[![Reddit](/assets/reddit.png){height:30px}](https://www.reddit.com/r/homebrewery/) [![Reddit](https://homebrewery.naturalcrit.com/assets/reddit.png){height:30px}](https://www.reddit.com/r/homebrewery/)
}} }}
\page \page
@@ -162,7 +162,7 @@ Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax. Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
![alt-text](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:100px,border:"2px solid",border-radius:10px} ![alt-text](https://homebrewery.naturalcrit.com/assets/catwarrior.jpg) {width:100px,border:"2px solid",border-radius:10px}
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.* \* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.*
+24 -24
View File
@@ -4,28 +4,28 @@ import './newPage.less';
// Common imports // Common imports
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'markdown.js'; import Markdown from '@shared/markdown.js';
import _ from 'lodash'; import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'client/homebrew/navbar/nav.jsx'; import Nav from '@navbar/nav.jsx';
import Navbar from 'client/homebrew/navbar/navbar.jsx'; import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from 'client/homebrew/navbar/newbrew.navitem.jsx'; import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from 'client/homebrew/navbar/account.navitem.jsx'; import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx'; import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx'; import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx'; import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from 'client/homebrew/navbar/print.navitem.jsx'; import PrintNavItem from '@navbar/print.navitem.jsx';
import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx'; import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports // Page specific imports
import { Meta } from 'vitreum/headtags';
const BREWKEY = 'HB_newPage_content'; const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style'; const STYLEKEY = 'HB_newPage_style';
@@ -42,23 +42,23 @@ const NewPage = (props)=>{
...props ...props
}; };
const [currentBrew , setCurrentBrew ] = useState(props.brew); const [currentBrew, setCurrentBrew] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false); const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
const [error , setError ] = useState(null); const [error, setError] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false); const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));
// const saveTimeout = useRef(null); // const saveTimeout = useRef(null);
// const warnUnsavedTimeout = useRef(null); // const warnUnsavedTimeout = useRef(null);
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const trySaveRef = useRef(null); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{ useEffect(()=>{
@@ -276,4 +276,4 @@ const NewPage = (props)=>{
); );
}; };
module.exports = NewPage; export default NewPage;
@@ -1,3 +1,5 @@
@import '@sharedStyles/colors.less';
.newPage { .newPage {
.navItem.save { .navItem.save {
background-color : @orange; background-color : @orange;
+15 -14
View File
@@ -1,18 +1,19 @@
require('./sharePage.less'); import './sharePage.less';
const React = require('react'); import React, { useState, useEffect, useCallback } from 'react';
const { useState, useEffect, useCallback } = React; import Headtags from '../../../../vitreum/headtags.js';
const { Meta } = require('vitreum/headtags'); const Meta = Headtags.Meta;
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from '@navbar/nav.jsx';
const Navbar = require('client/homebrew/navbar/navbar.jsx'); import Navbar from '@navbar/navbar.jsx';
const MetadataNav = require('client/homebrew/navbar/metadata.navitem.jsx'); import MetadataNav from '@navbar/metadata.navitem.jsx';
const PrintNavItem = require('client/homebrew/navbar/print.navitem.jsx'); import PrintNavItem from '@navbar/print.navitem.jsx';
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both; import RecentNavItems from '@navbar/recent.navitem.jsx';
const Account = require('client/homebrew/navbar/account.navitem.jsx'); const { both: RecentNavItem } = RecentNavItems;
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); import Account from '@navbar/account.navitem.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
const SharePage = (props)=>{ const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props; const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
@@ -116,4 +117,4 @@ const SharePage = (props)=>{
); );
}; };
module.exports = SharePage; export default SharePage;
+13 -13
View File
@@ -1,17 +1,17 @@
const React = require('react'); import React, { useState } from 'react';
const { useState } = React; import _ from 'lodash';
const _ = require('lodash');
const ListPage = require('../basePages/listPage/listPage.jsx'); import ListPage from '../basePages/listPage/listPage.jsx';
const Nav = require('client/homebrew/navbar/nav.jsx'); import Nav from '@navbar/nav.jsx';
const Navbar = require('client/homebrew/navbar/navbar.jsx'); import Navbar from '@navbar/navbar.jsx';
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both; import RecentNavItems from '@navbar/recent.navitem.jsx';
const Account = require('client/homebrew/navbar/account.navitem.jsx'); const { both: RecentNavItem } = RecentNavItems;
const NewBrew = require('client/homebrew/navbar/newbrew.navitem.jsx'); import Account from '@navbar/account.navitem.jsx';
const HelpNavItem = require('client/homebrew/navbar/help.navitem.jsx'); import NewBrew from '@navbar/newbrew.navitem.jsx';
const ErrorNavItem = require('client/homebrew/navbar/error-navitem.jsx'); import HelpNavItem from '@navbar/help.navitem.jsx';
const VaultNavitem = require('client/homebrew/navbar/vault.navitem.jsx'); import ErrorNavItem from '@navbar/error-navitem.jsx';
import VaultNavitem from '@navbar/vault.navitem.jsx';
const UserPage = (props)=>{ const UserPage = (props)=>{
props = { props = {
@@ -61,4 +61,4 @@ const UserPage = (props)=>{
); );
}; };
module.exports = UserPage; export default UserPage;
+13 -14
View File
@@ -1,19 +1,18 @@
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
/*eslint max-params:["warn", { max: 10 }], */ /*eslint max-params:["warn", { max: 10 }], */
require('./vaultPage.less'); import './vaultPage.less';
import React, { useState, useEffect, useRef } from 'react';
const React = require('react'); import Nav from '@navbar/nav.jsx';
const { useState, useEffect, useRef } = React; import Navbar from '@navbar/navbar.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const Nav = require('client/homebrew/navbar/nav.jsx'); const { both: RecentNavItem } = RecentNavItems;
const Navbar = require('client/homebrew/navbar/navbar.jsx'); import Account from '@navbar/account.navitem.jsx';
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both; import NewBrew from '@navbar/newbrew.navitem.jsx';
const Account = require('client/homebrew/navbar/account.navitem.jsx'); import HelpNavItem from '@navbar/help.navitem.jsx';
const NewBrew = require('client/homebrew/navbar/newbrew.navitem.jsx'); import BrewItem from '../basePages/listPage/brewItem/brewItem.jsx';
const HelpNavItem = require('client/homebrew/navbar/help.navitem.jsx'); import SplitPane from '../../../components/splitPane/splitPane.jsx';
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx'); import ErrorIndex from '../errorPage/errors/errorIndex.js';
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
@@ -430,4 +429,4 @@ const VaultPage = (props)=>{
); );
}; };
module.exports = VaultPage; export default VaultPage;
@@ -1,3 +1,5 @@
@import '@sharedStyles/core.less';
.vaultPage { .vaultPage {
height : 100%; height : 100%;
overflow-y : hidden; overflow-y : hidden;
-33
View File
@@ -1,33 +0,0 @@
const template = async function(name, title='', props = {}){
const ogTags = [];
const ogMeta = props.ogMeta ?? {};
Object.entries(ogMeta).forEach(([key, value])=>{
if(!value) return;
const tag = `<meta property="og:${key}" content="${value}">`;
ogTags.push(tag);
});
const ogMetaTags = ogTags.join('\n');
const ssrModule = await import(`../build/${name}/ssr.cjs`);
return `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
${ogMetaTags}
<meta name="twitter:card" content="summary">
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
</head>
<body>
<main id="reactRoot">${ssrModule.default(props)}</main>
<script src=${`/${name}/bundle.js`}></script>
<script>start_app(${JSON.stringify(props)})</script>
</body>
</html>
`;
};
export default template;
+1 -1
View File
@@ -32,7 +32,7 @@ export default [{
"max-depth" : ["warn", { max: 4 }], "max-depth" : ["warn", { max: 4 }],
"max-params" : ["warn", { max: 5 }], "max-params" : ["warn", { max: 5 }],
"no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"], "no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"],
"no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createClass" }], "no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createReactClass" }],
"react/jsx-uses-vars" : "warn", "react/jsx-uses-vars" : "warn",
/** Fixable **/ /** Fixable **/
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link
href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700"
rel="stylesheet"
type="text/css" />
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
<meta name="twitter:card" content="summary" />
<title>The Homebrewery - NaturalCrit</title>
</head>
<body>
<main id="reactRoot"></main>
<script type="module">
if (window.location.pathname.startsWith('/admin')) {
import('/client/admin/main.jsx');
} else {
import('/client/homebrew/main.jsx');
}
</script>
</body>
</html>
+36 -34
View File
@@ -1,21 +1,19 @@
{ {
"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.20.0", "version": "3.20.1",
"type": "module", "type": "module",
"engines": { "engines": {
"npm": "^10.8.x", "npm": ">=10.8 <12",
"node": "^20.18.x" "node": ">=20.18 <25"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/naturalcrit/homebrewery.git" "url": "git://github.com/naturalcrit/homebrewery.git"
}, },
"scripts": { "scripts": {
"dev": "node --experimental-require-module scripts/dev.js", "start": "node server.js",
"quick": "node --experimental-require-module scripts/quick.js", "build": "vite build",
"build": "node --experimental-require-module scripts/buildHomebrew.js && node --experimental-require-module scripts/buildAdmin.js",
"builddev": "node --experimental-require-module scripts/buildHomebrew.js --dev",
"lint": "eslint --fix", "lint": "eslint --fix",
"lint:dry": "eslint", "lint:dry": "eslint",
"stylelint": "stylelint --fix **/*.{less}", "stylelint": "stylelint --fix **/*.{less}",
@@ -44,7 +42,6 @@
"phb": "node --experimental-require-module scripts/phb.js", "phb": "node --experimental-require-module scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build", "prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run build", "postinstall": "npm run build",
"start": "node --experimental-require-module server.js",
"docker:build": "docker build -t ${DOCKERID}/homebrewery:$npm_package_version .", "docker:build": "docker build -t ${DOCKERID}/homebrewery:$npm_package_version .",
"docker:publish": "docker login && docker push ${DOCKERID}/homebrewery:$npm_package_version" "docker:publish": "docker login && docker push ${DOCKERID}/homebrewery:$npm_package_version"
}, },
@@ -61,8 +58,12 @@
"server" "server"
], ],
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!nanoid/).*" "node_modules/(?!(nanoid|@exodus/bytes|parse5|@asamuzakjp|@csstools)/)"
], ],
"transform": {
"^.+\\.[jt]s$": "babel-jest",
"^.+\\.mjs$": "babel-jest"
},
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
"build/*" "build/*"
], ],
@@ -85,14 +86,15 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.28.4", "@babel/core": "^7.29.0",
"@babel/plugin-transform-runtime": "^7.28.3", "@babel/plugin-transform-runtime": "^7.29.0",
"@babel/preset-env": "^7.28.3", "@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5", "@babel/preset-react": "^7.28.5",
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.6",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^19.2.0", "@googleapis/drive": "^20.1.0",
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"@vitejs/plugin-react": "^5.1.2",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
@@ -100,18 +102,17 @@
"core-js": "^3.47.0", "core-js": "^3.47.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent": "^1.7.1",
"expr-eval": "^2.0.2",
"express": "^5.1.0", "express": "^5.1.0",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.0", "express-static-gzip": "3.0.0",
"fflate": "^0.8.2", "fflate": "^0.8.2",
"fs-extra": "11.3.2", "fs-extra": "^11.3.3",
"hash-wasm": "^4.12.0", "hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^4.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "15.0.12", "marked": "15.0.12",
"marked-alignment-paragraphs": "^1.0.0", "marked-alignment-paragraphs": "^1.0.0",
@@ -123,36 +124,37 @@
"marked-nonbreaking-spaces": "^1.0.1", "marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3", "marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.4", "marked-subsuper-text": "^1.0.4",
"marked-variables": "^1.0.4", "marked-variables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^8.20.0", "mongoose": "^9.2.1",
"nanoid": "5.1.6", "nanoid": "5.1.6",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"react": "^18.3.1", "node": "^25.7.0",
"react-dom": "^18.3.1", "react": "^19.2.4",
"react-frame-component": "^4.1.3", "react-dom": "^19.2.4",
"react-router": "^7.9.6", "react-frame-component": "^5.2.7",
"romans": "^3.1.0", "react-router": "^7.13.1",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^10.2.1", "superagent": "^10.2.1"
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
"written-number": "^0.11.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^4.0.0", "@stylistic/stylelint-plugin": "^5.0.1",
"babel-jest": "^30.2.0",
"babel-plugin-transform-import-meta": "^2.3.3", "babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.39.1", "eslint": "9.7",
"eslint-plugin-jest": "^29.1.0", "eslint-plugin-jest": "^29.1.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.4.0", "globals": "^16.4.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom": "^28.1.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.25.0", "stylelint": "^17.4.0",
"stylelint-config-recess-order": "^7.3.0", "stylelint-config-recess-order": "^7.6.1",
"stylelint-config-recommended": "^17.0.0", "stylelint-config-recommended": "^18.0.0",
"supertest": "^7.1.4" "supertest": "^7.1.4",
"vite": "^7.3.1"
} }
} }
-32
View File
@@ -1,32 +0,0 @@
import fs from 'fs-extra';
import Proj from './project.json' with { type: 'json' };
import vitreum from 'vitreum';
const { pack } = vitreum;
import lessTransform from 'vitreum/transforms/less.js';
import assetTransform from 'vitreum/transforms/asset.js';
const isDev = !!process.argv.find((arg)=>arg=='--dev');
const transforms = {
'.less' : lessTransform,
'*' : assetTransform('./build')
};
const build = async ({ bundle, render, ssr })=>{
const css = await lessTransform.generate({ paths: './shared' });
await fs.outputFile('./build/admin/bundle.css', css);
await fs.outputFile('./build/admin/bundle.js', bundle);
await fs.outputFile('./build/admin/ssr.cjs', ssr);
};
fs.emptyDirSync('./build/admin');
pack('./client/admin/admin.jsx', {
paths : ['./shared'],
libs : Proj.libs,
dev : isDev && build,
transforms
})
.then(build)
.catch(console.error);
-169
View File
@@ -1,169 +0,0 @@
import fs from 'fs-extra';
import zlib from 'zlib';
import Proj from './project.json' with { type: 'json' };
import vitreum from 'vitreum';
const { pack, watchFile, livereload } = vitreum;
import lessTransform from 'vitreum/transforms/less.js';
import assetTransform from 'vitreum/transforms/asset.js';
import babel from '@babel/core';
import babelConfig from '../babel.config.json' with { type : 'json' };
import less from 'less';
const isDev = !!process.argv.find((arg)=>arg === '--dev');
const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
const transforms = {
'.js' : (code, filename, opts)=>babelify(code),
'.jsx' : (code, filename, opts)=>babelify(code),
'.less' : lessTransform,
'*' : assetTransform('./build')
};
const build = async ({ bundle, render, ssr })=>{
const css = await lessTransform.generate({ paths: './shared' });
//css = `@layer bundle {\n${css}\n}`;
await fs.outputFile('./build/homebrew/bundle.css', css);
await fs.outputFile('./build/homebrew/bundle.js', bundle);
await fs.outputFile('./build/homebrew/ssr.cjs', ssr);
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
//compress files in production
if(!isDev){
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
} else {
await fs.remove('./build/homebrew/bundle.css.br');
await fs.remove('./build/homebrew/bundle.js.br');
await fs.remove('./build/homebrew/ssr.js.br');
}
};
fs.emptyDirSync('./build');
(async ()=>{
//v==----------------------------- COMPILE THEMES --------------------------------==v//
// Update list of all Theme files
const themes = { Legacy: {}, V3: {} };
let themeFiles = fs.readdirSync('./themes/Legacy');
for (const dir of themeFiles) {
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
themeData.path = dir;
themes.Legacy[dir] = (themeData);
//fs.copy(`./themes/Legacy/${dir}/dropdownTexture.png`, `./build/themes/Legacy/${dir}/dropdownTexture.png`);
const src = `./themes/Legacy/${dir}/style.less`;
((outputDirectory)=>{
less.render(fs.readFileSync(src).toString(), {
compress : !isDev
}, function(e, output) {
fs.outputFile(outputDirectory, output.css);
});
})(`./build/themes/Legacy/${dir}/style.css`);
}
themeFiles = fs.readdirSync('./themes/V3');
for (const dir of themeFiles) {
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
themeData.path = dir;
themes.V3[dir] = (themeData);
fs.copy(`./themes/V3/${dir}/dropdownTexture.png`, `./build/themes/V3/${dir}/dropdownTexture.png`);
fs.copy(`./themes/V3/${dir}/dropdownPreview.png`, `./build/themes/V3/${dir}/dropdownPreview.png`);
const src = `./themes/V3/${dir}/style.less`;
((outputDirectory)=>{
less.render(fs.readFileSync(src).toString(), {
compress : !isDev
}, function(e, output) {
fs.outputFile(outputDirectory, output.css);
});
})(`./build/themes/V3/${dir}/style.css`);
}
await fs.outputFile('./themes/themes.json', JSON.stringify(themes, null, 2));
// await less.render(lessCode, {
// compress : !dev,
// sourceMap : (dev ? {
// sourceMapFileInline: true,
// outputSourceFiles: true
// } : false),
// })
// Move assets
await fs.copy('./themes/fonts', './build/fonts');
await fs.copy('./themes/assets', './build/assets');
await fs.copy('./client/icons', './build/icons');
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
const editorThemesBuildDir = './build/homebrew/cm-themes';
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
const editorThemeFile = './themes/codeMirror/editorThemes.json';
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
stream.write('[\n"default"');
for (const themeFile of editorThemeFiles) {
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
}
stream.write('\n]\n');
stream.end();
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
const bundles = await pack('./client/homebrew/homebrew.jsx', {
paths : ['./shared', './'],
libs : Proj.libs,
dev : isDev && build,
transforms
});
build(bundles);
// Possible method for generating separate bundles for theme snippets: factor-bundle first sending all common files to bundle.js, then again using default settings, keeping only snippet bundles
// await fs.outputFile('./build/junk.js', '');
// await fs.outputFile('./build/themes/Legacy/5ePHB/snippets.js', '');
//
// const files = ['./client/homebrew/homebrew.jsx','./themes/Legacy/5ePHB/snippets.js'];
//
// bundles = await pack(files, {
// dedupe: false,
// plugin : [['factor-bundle', { outputs: [ './build/junk.js','./build/themes/Legacy/5ePHB/snippets.js'], threshold : function(row, groups) {
// console.log(groups);
// if (groups.some(group => /.*homebrew.jsx$/.test(group))) {
// console.log("found homebrewery")
// return true;
// }
// return this._defaultThreshold(row, groups);
// }}]],
// paths : ['./shared','./','./build'],
// libs : Proj.libs,
// dev : isDev && build,
// transforms
// });
// build(bundles);
//
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
if(isDev){
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed
});
}
})().catch(console.error);
-22
View File
@@ -1,22 +0,0 @@
const label = 'dev';
console.time(label);
const jsx = require('vitreum/steps/jsx.watch.js');
const less = require('vitreum/steps/less.watch.js');
const assets = require('vitreum/steps/assets.watch.js');
const server = require('vitreum/steps/server.watch.js');
const livereload = require('vitreum/steps/livereload.js');
const Proj = require('./project.json');
Promise.resolve()
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] }))
.then((deps)=>less('homebrew', { shared: ['./shared'] }, deps))
.then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] }))
.then((deps)=>less('admin', { shared: ['./shared'] }, deps))
.then(()=>assets(Proj.assets, ['./shared', './client']))
.then(()=>livereload())
.then(()=>server('./server.js', ['server']))
.then(console.timeEnd.bind(console, label))
.catch(console.error);
-17
View File
@@ -1,17 +0,0 @@
const label = 'quick';
console.time(label);
const jsx = require('vitreum/steps/jsx.js').partial;
const less = require('vitreum/steps/less.js').partial;
const server = require('vitreum/steps/server.watch.js').partial;
const Proj = require('./project.json');
Promise.resolve()
.then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, ['./shared']))
.then(less('homebrew', ['./shared']))
.then(jsx('admin', './client/admin/admin.jsx', Proj.libs, ['./shared']))
.then(less('admin', ['./shared']))
.then(server('./server.js', ['server']))
.then(console.timeEnd.bind(console, label))
.catch(console.error);
+30 -10
View File
@@ -1,12 +1,29 @@
import DB from './server/db.js'; import DB from './server/db.js';
import server from './server/app.js'; import createApp from './server/app.js';
import config from './server/config.js'; import config from './server/config.js';
import { createServer as createViteServer } from 'vite';
DB.connect(config).then(()=>{ const isDev = process.env.NODE_ENV === 'local';
// Ensure that we have successfully connected to the database
// before launching server async function start() {
const PORT = process.env.PORT || config.get('web_port') || 8000; let vite;
server.listen(PORT, ()=>{
if(isDev) {
vite = await createViteServer({
server : { middlewareMode: true },
appType : 'custom',
});
}
await DB.connect(config).catch((err)=>{
console.error('Database connection failed:', err);
process.exit(1);
});
const app = await createApp(vite);
const PORT = process.env.PORT || config.get('web_port') || 3000;
app.listen(PORT, ()=>{
const reset = '\x1b[0m'; // Reset to default style const reset = '\x1b[0m'; // Reset to default style
const bright = '\x1b[1m'; // Bright (bold) style const bright = '\x1b[1m'; // Bright (bold) style
const cyan = '\x1b[36m'; // Cyan color const cyan = '\x1b[36m'; // Cyan color
@@ -14,7 +31,10 @@ DB.connect(config).then(()=>{
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`); console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
console.log(`\tserver on port: ${PORT}`); console.log(`\tserver on port: ${PORT}`);
console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`); console.log(
`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`,
);
}); });
}); }
start();
+245 -227
View File
@@ -4,64 +4,69 @@ import { model as NotificationModel } from './notifications.model.js';
import express from 'express'; import express from 'express';
import Moment from 'moment'; import Moment from 'moment';
import zlib from 'zlib'; import zlib from 'zlib';
import templateFn from '../client/template.js'; import config from './config.js';
import path from 'path';
import fs from 'fs-extra';
const nodeEnv = config.get('node_env');
const isProd = nodeEnv === 'production';
import HomebrewAPI from './homebrew.api.js'; import HomebrewAPI from './homebrew.api.js';
import asyncHandler from 'express-async-handler'; import asyncHandler from 'express-async-handler';
import { splitTextStyleAndMetadata } from '../shared/helpers.js'; import { splitTextStyleAndMetadata } from '../shared/helpers.js';
const router = express.Router();
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin'; process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3'; process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
const mw = { export default function createAdminApi(vite) {
adminOnly : (req, res, next)=>{ const router = express.Router();
if(!req.get('authorization')){
return res const mw = {
adminOnly : (req, res, next)=>{
if(!req.get('authorization')){
return res
.set('WWW-Authenticate', 'Basic realm="Authorization Required"') .set('WWW-Authenticate', 'Basic realm="Authorization Required"')
.status(401) .status(401)
.send('Authorization Required'); .send('Authorization Required');
} }
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64') const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
.toString('ascii') .toString('ascii')
.split(':'); .split(':');
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){ if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
return next(); return next();
}
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
} }
throw { HBErrorCode: '52', code: 401, message: 'Access denied' }; };
}
};
const junkBrewPipeline = [ const junkBrewPipeline = [
{ $match : { { $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() } lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
} }, } },
{ $project: { textBinSize: { $binarySize: '$textBin' } } }, { $project: { textBinSize: { $binarySize: '$textBin' } } },
{ $match: { textBinSize: { $lt: 140 } } }, { $match: { textBinSize: { $lt: 140 } } },
{ $limit: 100 } { $limit: 100 }
]; ];
/* Search for brews that aren't compressed (missing the compressed text field) */ /* Search for brews that aren't compressed (missing the compressed text field) */
const uncompressedBrewQuery = HomebrewModel.find({ const uncompressedBrewQuery = HomebrewModel.find({
'text' : { '$exists': true } 'text' : { '$exists': true }
}).lean().limit(10000).select('_id'); }).lean().limit(10000).select('_id');
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes // Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{ router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length })) .then((objs)=>res.json({ count: objs.length }))
.catch((error)=>{ .catch((error)=>{
console.error(error); console.error(error);
res.status(500).json({ error: 'Internal Server Error' }); res.status(500).json({ error: 'Internal Server Error' });
}); });
}); });
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes // Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((docs)=>{ .then((docs)=>{
const ids = docs.map((doc)=>doc._id); const ids = docs.map((doc)=>doc._id);
return HomebrewModel.deleteMany({ _id: { $in: ids } }); return HomebrewModel.deleteMany({ _id: { $in: ids } });
@@ -71,18 +76,18 @@ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
console.error(error); console.error(error);
res.status(500).json({ error: 'Internal Server Error' }); res.status(500).json({ error: 'Internal Server Error' });
}); });
}); });
/* Searches for matching edit or share id, also attempts to partial match */ /* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{ router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
return res.json(req.brew); return res.json(req.brew);
}); });
/* Find 50 brews that aren't compressed yet */ /* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
const query = uncompressedBrewQuery.clone(); const query = uncompressedBrewQuery.clone();
query.exec() query.exec()
.then((objs)=>{ .then((objs)=>{
const ids = objs.map((obj)=>obj._id); const ids = objs.map((obj)=>obj._id);
res.json({ count: ids.length, ids }); res.json({ count: ids.length, ids });
@@ -91,46 +96,46 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
console.error(err); console.error(err);
res.status(500).send(err.message || 'Internal Server Error'); res.status(500).send(err.message || 'Internal Server Error');
}); });
});
/* Cleans `<script` and `</script>` from the "text" field of a brew */
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
const brew = req.brew;
const properties = ['text', 'description', 'title'];
properties.forEach((property)=>{
brew[property] = cleanText(brew[property]);
}); });
splitTextStyleAndMetadata(brew); /* Cleans `<script` and `</script>` from the "text" field of a brew */
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
req.body = brew; function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
// Remove Account from request to prevent Admin user from being added to brew as an Author const brew = req.brew;
req.account = undefined;
return await HomebrewAPI.updateBrew(req, res); const properties = ['text', 'description', 'title'];
}); properties.forEach((property)=>{
brew[property] = cleanText(brew[property]);
});
/* Get list of a user's documents */ splitTextStyleAndMetadata(brew);
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
const username = req.params.user;
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`); req.body = brew;
const brews = await HomebrewModel.getByUser(username, true, fields); // Remove Account from request to prevent Admin user from being added to brew as an Author
req.account = undefined;
return res.json(brews); return await HomebrewAPI.updateBrew(req, res);
}); });
/* Compresses the "text" field of a brew to binary */ /* Get list of a user's documents */
router.put('/admin/compress/:id', (req, res)=>{ router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
HomebrewModel.findOne({ _id: req.params.id }) const username = req.params.user;
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`);
const brews = await HomebrewModel.getByUser(username, true, fields);
return res.json(brews);
});
/* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{
HomebrewModel.findOne({ _id: req.params.id })
.then((brew)=>{ .then((brew)=>{
if(!brew) if(!brew)
return res.status(404).send('Brew not found'); return res.status(404).send('Brew not found');
@@ -147,239 +152,252 @@ router.put('/admin/compress/:id', (req, res)=>{
console.error(err); console.error(err);
res.status(500).send('Error while saving'); res.status(500).send('Error while saving');
}); });
}); });
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try { try {
const totalBrewsCount = await HomebrewModel.countDocuments({}); const totalBrewsCount = await HomebrewModel.countDocuments({});
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true }); const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
return res.json({ return res.json({
totalBrews : totalBrewsCount, totalBrews : totalBrewsCount,
totalPublishedBrews : publishedBrewsCount totalPublishedBrews : publishedBrewsCount
}); });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return res.status(500).json({ error: 'Internal Server Error' }); return res.status(500).json({ error: 'Internal Server Error' });
} }
}); });
// ####################### LOCKS // ####################### LOCKS
router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{ router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
const countLocksQuery = { const countLocksQuery = {
lock : { $exists: true } lock : { $exists: true }
}; };
const count = await HomebrewModel.countDocuments(countLocksQuery) const count = await HomebrewModel.countDocuments(countLocksQuery)
.catch((error)=>{ .catch((error)=>{
throw { name: 'Lock Count Error', message: 'Unable to get lock count', status: 500, HBErrorCode: '61', error }; throw { name: 'Lock Count Error', message: 'Unable to get lock count', status: 500, HBErrorCode: '61', error };
}); });
return res.json({ count }); return res.json({ count });
})); }));
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{ router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
const countLocksPipeline = [ const countLocksPipeline = [
{ {
$match : $match :
{ {
'lock' : { '$exists': 1 } 'lock' : { '$exists': 1 }
}, },
}, },
{ {
$project : { $project : {
shareId : 1, shareId : 1,
editId : 1, editId : 1,
title : 1, title : 1,
lock : 1 lock : 1
}
} }
} ];
]; const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
.catch((error)=>{ .catch((error)=>{
throw { name: 'Can Not Get Locked Brews', message: 'Unable to get locked brew collection', status: 500, HBErrorCode: '68', error }; throw { name: 'Can Not Get Locked Brews', message: 'Unable to get locked brew collection', status: 500, HBErrorCode: '68', error };
}); });
return res.json({ return res.json({
lockedDocuments lockedDocuments
}); });
})); }));
router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{ router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
const lock = req.body; const lock = req.body;
lock.applied = new Date; lock.applied = new Date;
const filter = { const filter = {
shareId : req.params.id shareId : req.params.id
}; };
const brew = await HomebrewModel.findOne(filter); const brew = await HomebrewModel.findOne(filter);
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' }; if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' };
if(brew.lock && !lock.overwrite) { if(brew.lock && !lock.overwrite) {
throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' }; throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' };
} }
lock.overwrite = undefined; lock.overwrite = undefined;
brew.lock = lock; brew.lock = lock;
brew.markModified('lock'); brew.markModified('lock');
await brew.save() await brew.save()
.catch((error)=>{ .catch((error)=>{
throw { name: 'Lock Error', message: 'Unable to set lock', shareId: req.params.id, status: 500, HBErrorCode: '62', error }; throw { name: 'Lock Error', message: 'Unable to set lock', shareId: req.params.id, status: 500, HBErrorCode: '62', error };
}); });
return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock }); return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock });
})); }));
router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{ router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
const filter = { const filter = {
shareId : req.params.id shareId : req.params.id
}; };
const brew = await HomebrewModel.findOne(filter); const brew = await HomebrewModel.findOne(filter);
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' }; if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' };
if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' }; if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' };
brew.lock = undefined; brew.lock = undefined;
brew.markModified('lock'); brew.markModified('lock');
await brew.save() await brew.save()
.catch((error)=>{ .catch((error)=>{
throw { name: 'Cannot Unlock', message: 'Unable to clear lock', shareId: req.params.id, status: 500, HBErrorCode: '65', error }; throw { name: 'Cannot Unlock', message: 'Unable to clear lock', shareId: req.params.id, status: 500, HBErrorCode: '65', error };
}); });
return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` }); return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` });
})); }));
router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{ router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
const countReviewsPipeline = [ const countReviewsPipeline = [
{ {
$match : $match :
{ {
'lock.reviewRequested' : { '$exists': 1 } 'lock.reviewRequested' : { '$exists': 1 }
}, },
}, },
{ {
$project : { $project : {
shareId : 1, shareId : 1,
editId : 1, editId : 1,
title : 1, title : 1,
lock : 1 lock : 1
}
} }
} ];
]; const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
.catch((error)=>{ .catch((error)=>{
throw { name: 'Can Not Get Reviews', message: 'Unable to get review collection', status: 500, HBErrorCode: '68', error }; throw { name: 'Can Not Get Reviews', message: 'Unable to get review collection', status: 500, HBErrorCode: '68', error };
}); });
return res.json({ return res.json({
reviewDocuments reviewDocuments
}); });
})); }));
router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{ router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
// === This route is NOT Admin only === // === This route is NOT Admin only ===
// Any user can request a review of their document // Any user can request a review of their document
const filter = { const filter = {
shareId : req.params.id, shareId : req.params.id,
lock : { $exists: 1 } lock : { $exists: 1 }
}; };
const brew = await HomebrewModel.findOne(filter); const brew = await HomebrewModel.findOne(filter);
if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; }; if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; };
if(brew.lock.reviewRequested){ if(brew.lock.reviewRequested){
throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' }; throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' };
}; };
brew.lock.reviewRequested = new Date(); brew.lock.reviewRequested = new Date();
brew.markModified('lock'); brew.markModified('lock');
await brew.save() await brew.save()
.catch((error)=>{ .catch((error)=>{
throw { name: 'Can Not Set Review Request', message: `Unable to set request for review on brew ID ${req.params.id}`, code: 500, HBErrorCode: '69', error }; throw { name: 'Can Not Set Review Request', message: `Unable to set request for review on brew ID ${req.params.id}`, code: 500, HBErrorCode: '69', error };
}); });
return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` }); return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` });
})); }));
router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{ router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
const filter = { const filter = {
shareId : req.params.id, shareId : req.params.id,
'lock.reviewRequested' : { $exists: 1 } 'lock.reviewRequested' : { $exists: 1 }
}; };
const brew = await HomebrewModel.findOne(filter); const brew = await HomebrewModel.findOne(filter);
if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; }; if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; };
brew.lock.reviewRequested = undefined; brew.lock.reviewRequested = undefined;
brew.markModified('lock'); brew.markModified('lock');
await brew.save() await brew.save()
.catch((error)=>{ .catch((error)=>{
throw { name: 'Can Not Clear Review Request', message: `Unable to remove request for review on brew ID ${req.params.id}`, HBErrorCode: '72', error }; throw { name: 'Can Not Clear Review Request', message: `Unable to remove request for review on brew ID ${req.params.id}`, HBErrorCode: '72', error };
}); });
return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` }); return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` });
})); }));
// ####################### NOTIFICATIONS // ####################### NOTIFICATIONS
router.get('/admin/notification/all', async (req, res, next)=>{ router.get('/admin/notification/all', async (req, res, next)=>{
try { try {
const notifications = await NotificationModel.getAll(); const notifications = await NotificationModel.getAll();
return res.json(notifications); return res.json(notifications);
} catch (error) { } catch (error) {
console.log('Error getting all notifications: ', error.message); console.log('Error getting all notifications: ', error.message);
return res.status(500).json({ message: error.message }); return res.status(500).json({ message: error.message });
} }
});
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.addNotification(req.body);
return res.status(201).json(notification);
} catch (error) {
console.log('Error adding notification: ', error.message);
return res.status(500).json({ message: error.message });
}
});
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.deleteNotification(req.params.id);
return res.json(notification);
} catch (error) {
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
return res.status(500).json({ message: error.message });
}
});
router.get('/admin', mw.adminOnly, (req, res)=>{
templateFn('admin', {
url : req.originalUrl
})
.then((page)=>res.send(page))
.catch((err)=>{
console.log(err);
res.sendStatus(500);
}); });
});
export default router; router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.addNotification(req.body);
return res.status(201).json(notification);
} catch (error) {
console.log('Error adding notification: ', error.message);
return res.status(500).json({ message: error.message });
}
});
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.deleteNotification(req.params.id);
return res.json(notification);
} catch (error) {
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
return res.status(500).json({ message: error.message });
}
});
router.get('/admin', mw.adminOnly, asyncHandler(async (req, res)=>{
const props = {
url : req.originalUrl
};
const htmlPath = isProd
? path.resolve('build', 'index.html')
: path.resolve('index.html');
let html = fs.readFileSync(htmlPath, 'utf-8');
if(!isProd && vite?.transformIndexHtml) {
html = await vite.transformIndexHtml(req.originalUrl, html);
}
res.send(html.replace(
'<head>',
`<head>\n<script id="props">window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>`
));
}));
return router;
}
+167 -223
View File
@@ -1,30 +1,45 @@
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
import mongoose from 'mongoose';
import supertest from 'supertest'; import supertest from 'supertest';
import HBApp from './app.js'; import createApp from './app.js';
import { model as NotificationModel } from './notifications.model.js'; import { model as NotificationModel } from './notifications.model.js';
import { model as HomebrewModel } from './homebrew.model.js'; import { model as HomebrewModel } from './homebrew.model.js';
let app;
let request;
let dbState;
// Mimic https responses to avoid being redirected all the time beforeAll(async ()=>{
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https'); app = await createApp();
request = supertest.agent(app).set('X-Forwarded-Proto', 'https');
});
describe('Tests for admin api', ()=>{ describe('Tests for admin api', ()=>{
beforeEach(()=>{
dbState = mongoose.connection.readyState;
mongoose.connection.readyState = 1;
});
afterEach(()=>{ afterEach(()=>{
mongoose.connection.readyState = dbState;
jest.resetAllMocks(); jest.resetAllMocks();
}); });
afterAll(async ()=>{
await mongoose.connection.close();
});
describe('Notifications', ()=>{ describe('Notifications', ()=>{
it('should return list of all notifications', async ()=>{ it('should return list of all notifications', async ()=>{
const testNotifications = ['a', 'b']; const testNotifications = ['a', 'b'];
jest.spyOn(NotificationModel, 'find') jest.spyOn(NotificationModel, 'find').mockImplementationOnce(()=>{
.mockImplementationOnce(()=>{
return { exec: jest.fn().mockResolvedValue(testNotifications) }; return { exec: jest.fn().mockResolvedValue(testNotifications) };
}); });
const response = await app const response = await request
.get('/admin/notification/all') .get('/admin/notification/all')
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual(testNotifications); expect(response.body).toEqual(testNotifications);
@@ -44,18 +59,17 @@ describe('Tests for admin api', ()=>{
_id : expect.any(String), _id : expect.any(String),
createdAt : expect.any(String), createdAt : expect.any(String),
startAt : inputNotification.startAt, startAt : inputNotification.startAt,
stopAt : inputNotification.stopAt, stopAt : inputNotification.stopAt
}; };
jest.spyOn(NotificationModel.prototype, 'save') jest.spyOn(NotificationModel.prototype, 'save').mockImplementationOnce(function () {
.mockImplementationOnce(function() { return Promise.resolve(this);
return Promise.resolve(this); });
});
const response = await app const response = await request
.post('/admin/notification/add') .post('/admin/notification/add')
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
.send(inputNotification); .send(inputNotification);
expect(response.status).toBe(201); expect(response.status).toBe(201);
expect(response.body).toEqual(savedNotification); expect(response.body).toEqual(savedNotification);
@@ -69,16 +83,14 @@ describe('Tests for admin api', ()=>{
stopAt : new Date().toISOString() stopAt : new Date().toISOString()
}; };
//Change 'save' function to just return itself instead of actually interacting with the database jest.spyOn(NotificationModel.prototype, 'save').mockImplementationOnce(function () {
jest.spyOn(NotificationModel.prototype, 'save') return Promise.resolve(this);
.mockImplementationOnce(function() { });
return Promise.resolve(this);
});
const response = await app const response = await request
.post('/admin/notification/add') .post('/admin/notification/add')
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
.send(inputNotification); .send(inputNotification);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Dismiss key is required!' }); expect(response.body).toEqual({ message: 'Dismiss key is required!' });
@@ -87,15 +99,15 @@ describe('Tests for admin api', ()=>{
it('should delete a notification based on its dismiss key', async ()=>{ it('should delete a notification based on its dismiss key', async ()=>{
const dismissKey = 'testKey'; const dismissKey = 'testKey';
jest.spyOn(NotificationModel, 'findOneAndDelete') jest.spyOn(NotificationModel, 'findOneAndDelete').mockImplementationOnce((key)=>{
.mockImplementationOnce((key)=>{ return { exec: jest.fn().mockResolvedValue(key) };
return { exec: jest.fn().mockResolvedValue(key) }; });
});
const response = await app
.delete(`/admin/notification/delete/${dismissKey}`)
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' }); const response = await request
.delete(`/admin/notification/delete/${dismissKey}`)
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ dismissKey: 'testKey' });
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual({ dismissKey: 'testKey' }); expect(response.body).toEqual({ dismissKey: 'testKey' });
}); });
@@ -103,15 +115,15 @@ describe('Tests for admin api', ()=>{
it('should handle error deleting a notification that doesnt exist', async ()=>{ it('should handle error deleting a notification that doesnt exist', async ()=>{
const dismissKey = 'testKey'; const dismissKey = 'testKey';
jest.spyOn(NotificationModel, 'findOneAndDelete') jest.spyOn(NotificationModel, 'findOneAndDelete').mockImplementationOnce(()=>{
.mockImplementationOnce(()=>{ return { exec: jest.fn().mockResolvedValue() };
return { exec: jest.fn().mockResolvedValue() }; });
});
const response = await app
.delete(`/admin/notification/delete/${dismissKey}`)
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' }); const response = await request
.delete(`/admin/notification/delete/${dismissKey}`)
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ dismissKey: 'testKey' });
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ message: 'Notification not found' }); expect(response.body).toEqual({ message: 'Notification not found' });
}); });
@@ -120,30 +132,24 @@ describe('Tests for admin api', ()=>{
describe('Locks', ()=>{ describe('Locks', ()=>{
describe('Count', ()=>{ describe('Count', ()=>{
it('Count of all locked documents', async ()=>{ it('Count of all locked documents', async ()=>{
const testNumber = 16777216; // 8^8, because why not const testNumber = 16777216;
jest.spyOn(HomebrewModel, 'countDocuments') jest.spyOn(HomebrewModel, 'countDocuments').mockImplementationOnce(()=>Promise.resolve(testNumber));
.mockImplementationOnce(()=>{
return Promise.resolve(testNumber);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .get('/api/lock/count')
.get('/api/lock/count'); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual({ count: testNumber }); expect(response.body).toEqual({ count: testNumber });
}); });
it('Handle error while fetching count of locked documents', async ()=>{ it('Handle error while fetching count of locked documents', async ()=>{
jest.spyOn(HomebrewModel, 'countDocuments') jest.spyOn(HomebrewModel, 'countDocuments').mockImplementationOnce(()=>Promise.reject());
.mockImplementationOnce(()=>{
return Promise.reject();
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .get('/api/lock/count')
.get('/api/lock/count'); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -151,7 +157,7 @@ describe('Tests for admin api', ()=>{
message : 'Unable to get lock count', message : 'Unable to get lock count',
name : 'Lock Count Error', name : 'Lock Count Error',
originalUrl : '/api/lock/count', originalUrl : '/api/lock/count',
status : 500, status : 500
}); });
}); });
}); });
@@ -160,28 +166,22 @@ describe('Tests for admin api', ()=>{
it('Get list of all locked documents', async ()=>{ it('Get list of all locked documents', async ()=>{
const testLocks = ['a', 'b']; const testLocks = ['a', 'b'];
jest.spyOn(HomebrewModel, 'aggregate') jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.resolve(testLocks));
.mockImplementationOnce(()=>{
return Promise.resolve(testLocks);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .get('/api/locks')
.get('/api/locks'); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual({ lockedDocuments: testLocks }); expect(response.body).toEqual({ lockedDocuments: testLocks });
}); });
it('Handle error while fetching list of all locked documents', async ()=>{ it('Handle error while fetching list of all locked documents', async ()=>{
jest.spyOn(HomebrewModel, 'aggregate') jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.reject());
.mockImplementationOnce(()=>{
return Promise.reject();
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .get('/api/locks')
.get('/api/locks'); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -196,28 +196,22 @@ describe('Tests for admin api', ()=>{
it('Get list of all locked documents with pending review requests', async ()=>{ it('Get list of all locked documents with pending review requests', async ()=>{
const testLocks = ['a', 'b']; const testLocks = ['a', 'b'];
jest.spyOn(HomebrewModel, 'aggregate') jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.resolve(testLocks));
.mockImplementationOnce(()=>{
return Promise.resolve(testLocks);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .get('/api/lock/reviews')
.get('/api/lock/reviews'); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual({ reviewDocuments: testLocks }); expect(response.body).toEqual({ reviewDocuments: testLocks });
}); });
it('Handle error while fetching list of all locked documents with pending review requests', async ()=>{ it('Handle error while fetching list of all locked documents with pending review requests', async ()=>{
jest.spyOn(HomebrewModel, 'aggregate') jest.spyOn(HomebrewModel, 'aggregate').mockImplementationOnce(()=>Promise.reject());
.mockImplementationOnce(()=>{
return Promise.reject();
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .get('/api/lock/reviews')
.get('/api/lock/reviews'); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -235,8 +229,8 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.resolve(); } save : ()=>Promise.resolve()
}; };
const testLock = { const testLock = {
@@ -245,15 +239,12 @@ describe('Tests for admin api', ()=>{
shareMessage : 'share' shareMessage : 'share'
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .post(`/api/lock/${testBrew.shareId}`)
.post(`/api/lock/${testBrew.shareId}`) .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
.send(testLock); .send(testLock);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toMatchObject({ expect(response.body).toMatchObject({
@@ -277,24 +268,21 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.resolve(); }, save : ()=>Promise.resolve(),
lock : { lock : {
code : 1, code : 1,
editMessage : 'oldEdit', editMessage : 'oldEdit',
shareMessage : 'oldShare', shareMessage : 'oldShare'
} }
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .post(`/api/lock/${testBrew.shareId}`)
.post(`/api/lock/${testBrew.shareId}`) .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
.send(testLock); .send(testLock);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toMatchObject({ expect(response.body).toMatchObject({
@@ -317,24 +305,21 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.resolve(); }, save : ()=>Promise.resolve(),
lock : { lock : {
code : 1, code : 1,
editMessage : 'oldEdit', editMessage : 'oldEdit',
shareMessage : 'oldShare', shareMessage : 'oldShare'
} }
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .post(`/api/lock/${testBrew.shareId}`)
.post(`/api/lock/${testBrew.shareId}`) .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
.send(testLock); .send(testLock);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -352,8 +337,8 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.reject(); } save : ()=>Promise.reject()
}; };
const testLock = { const testLock = {
@@ -362,15 +347,12 @@ describe('Tests for admin api', ()=>{
shareMessage : 'share' shareMessage : 'share'
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .post(`/api/lock/${testBrew.shareId}`)
.post(`/api/lock/${testBrew.shareId}`) .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
.send(testLock); .send(testLock);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -396,19 +378,17 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.resolve(); }, save : ()=>Promise.resolve(),
lock : testLock lock : testLock
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) 'Authorization',
.put(`/api/unlock/${testBrew.shareId}`); `Basic ${Buffer.from('admin:password3').toString('base64')}`
);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -421,18 +401,16 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.resolve(); }, save : ()=>Promise.resolve()
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) 'Authorization',
.put(`/api/unlock/${testBrew.shareId}`); `Basic ${Buffer.from('admin:password3').toString('base64')}`
);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -441,7 +419,7 @@ describe('Tests for admin api', ()=>{
name : 'Not Locked', name : 'Not Locked',
originalUrl : `/api/unlock/${testBrew.shareId}`, originalUrl : `/api/unlock/${testBrew.shareId}`,
shareId : testBrew.shareId, shareId : testBrew.shareId,
status : 500, status : 500
}); });
}); });
@@ -456,19 +434,17 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.reject(); }, save : ()=>Promise.reject(),
lock : testLock lock : testLock
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request.put(`/api/unlock/${testBrew.shareId}`).set(
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) 'Authorization',
.put(`/api/unlock/${testBrew.shareId}`); `Basic ${Buffer.from('admin:password3').toString('base64')}`
);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -494,40 +470,28 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.resolve(); }, save : ()=>Promise.resolve(),
lock : testLock lock : testLock
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
.put(`/api/lock/review/request/${testBrew.shareId}`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual({ expect(response.body).toEqual({
message : `Review requested on brew ID ${testBrew.shareId} - ${testBrew.title}`, message : `Review requested on brew ID ${testBrew.shareId} - ${testBrew.title}`,
name : 'Review Requested', name : 'Review Requested'
}); });
}); });
it('Error when cannot find a locked brew', async ()=>{ it('Error when cannot find a locked brew', async ()=>{
const testBrew = { const testBrew = { shareId: 'shareId' };
shareId : 'shareId'
};
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(false));
.mockImplementationOnce(()=>{
return Promise.resolve(false);
});
const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
const response = await app
.put(`/api/lock/review/request/${testBrew.shareId}`)
.catch((err)=>{return err;});
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -557,25 +521,20 @@ describe('Tests for admin api', ()=>{
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne')
.mockImplementationOnce(()=>{ .mockImplementationOnce(()=>Promise.resolve(testBrew));
return Promise.resolve(false);
});
const response = await request
const response = await app .put(`/api/lock/review/request/${testBrew.shareId}`);
.put(`/api/lock/review/request/${testBrew.shareId}`)
.catch((err)=>{return err;});
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
HBErrorCode : '70', HBErrorCode : '71',
code : 500, code : 500,
message : `Cannot find a locked brew with ID ${testBrew.shareId}`, message : `Review already requested for brew ${testBrew.shareId} - ${testBrew.title}`,
name : 'Brew Not Found', name : 'Review Already Requested',
originalUrl : `/api/lock/review/request/${testBrew.shareId}` originalUrl : `/api/lock/review/request/${testBrew.shareId}`
}); });
}); });
it('Handle error while adding review request to a locked brew', async ()=>{ it('Handle error while adding review request to a locked brew', async ()=>{
const testLock = { const testLock = {
applied : 'YES', applied : 'YES',
@@ -587,18 +546,14 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.reject(); }, save : ()=>Promise.reject(),
lock : testLock lock : testLock
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request.put(`/api/lock/review/request/${testBrew.shareId}`);
.put(`/api/lock/review/request/${testBrew.shareId}`);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -622,19 +577,16 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.resolve(); }, save : ()=>Promise.resolve(),
lock : testLock lock : testLock
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .put(`/api/lock/review/remove/${testBrew.shareId}`)
.put(`/api/lock/review/remove/${testBrew.shareId}`); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -644,18 +596,13 @@ describe('Tests for admin api', ()=>{
}); });
it('Error when clearing review request from a brew with no review request', async ()=>{ it('Error when clearing review request from a brew with no review request', async ()=>{
const testBrew = { const testBrew = { shareId: 'shareId' };
shareId : 'shareId',
};
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(false));
.mockImplementationOnce(()=>{
return Promise.resolve(false);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .put(`/api/lock/review/remove/${testBrew.shareId}`)
.put(`/api/lock/review/remove/${testBrew.shareId}`); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
@@ -678,19 +625,16 @@ describe('Tests for admin api', ()=>{
const testBrew = { const testBrew = {
shareId : 'shareId', shareId : 'shareId',
title : 'title', title : 'title',
markModified : ()=>{ return true; }, markModified : ()=>true,
save : ()=>{ return Promise.reject(); }, save : ()=>Promise.reject(),
lock : testLock lock : testLock
}; };
jest.spyOn(HomebrewModel, 'findOne') jest.spyOn(HomebrewModel, 'findOne').mockImplementationOnce(()=>Promise.resolve(testBrew));
.mockImplementationOnce(()=>{
return Promise.resolve(testBrew);
});
const response = await app const response = await request
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`) .put(`/api/lock/review/remove/${testBrew.shareId}`)
.put(`/api/lock/review/remove/${testBrew.shareId}`); .set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
expect(response.status).toBe(500); expect(response.status).toBe(500);
expect(response.body).toEqual({ expect(response.body).toEqual({
+550 -525
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More