0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-03 21:22:39 +00:00

Merge branch 'master' into fixUserPageLinks-#807

This commit is contained in:
Trevor Buckner
2025-11-14 17:19:45 -05:00
committed by GitHub
94 changed files with 4573 additions and 2232 deletions

View File

@@ -7,15 +7,17 @@ import LockTools from './lockTools/lockTools.jsx';
const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
const ADMIN_TAB = 'HB_adminPage_currentTab';
const Admin = ()=>{
const [currentTab, setCurrentTab] = useState('');
useEffect(()=>{
setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew');
setCurrentTab(localStorage.getItem(ADMIN_TAB) || 'brew');
}, []);
useEffect(()=>{
localStorage.setItem('hbAdminTab', currentTab);
localStorage.setItem(ADMIN_TAB, currentTab);
}, [currentTab]);
return (

View File

@@ -0,0 +1,84 @@
import diceFont from 'themes/fonts/iconFonts/diceFont.js';
import elderberryInn from 'themes/fonts/iconFonts/elderberryInn.js';
import fontAwesome from 'themes/fonts/iconFonts/fontAwesome.js';
import gameIcons from 'themes/fonts/iconFonts/gameIcons.js';
const emojis = {
...diceFont,
...elderberryInn,
...fontAwesome,
...gameIcons
};
const showAutocompleteEmoji = function(CodeMirror, editor) {
CodeMirror.commands.autocomplete = function(editor) {
editor.showHint({
completeSingle : false,
hint : function(editor) {
const cursor = editor.getCursor();
const line = cursor.line;
const lineContent = editor.getLine(line);
const start = lineContent.lastIndexOf(':', cursor.ch - 1) + 1;
const end = cursor.ch;
const currentWord = lineContent.slice(start, end);
const list = Object.keys(emojis).filter(function(emoji) {
return emoji.toLowerCase().indexOf(currentWord.toLowerCase()) >= 0;
}).sort((a, b)=>{
const lowerA = a.replace(/\d+/g, function(match) { // Temporarily convert any numbers in emoji string
return match.padStart(4, '0'); // to 4-digits, left-padded with 0's, to aid in
}).toLowerCase(); // sorting numbers, i.e., "d6, d10, d20", not "d10, d20, d6"
const lowerB = b.replace(/\d+/g, function(match) { // Also make lowercase for case-insensitive alpha sorting
return match.padStart(4, '0');
}).toLowerCase();
if(lowerA < lowerB)
return -1;
return 1;
}).map(function(emoji) {
return {
text : `${emoji}:`, // Text to output to editor when option is selected
render : function(element, self, data) { // How to display the option in the dropdown
const div = document.createElement('div');
div.innerHTML = `<i class="emojiPreview ${emojis[emoji]}"></i> ${emoji}`;
element.appendChild(div);
}
};
});
return {
list : list.length ? list : [],
from : CodeMirror.Pos(line, start),
to : CodeMirror.Pos(line, end)
};
}
});
};
editor.on('inputRead', function(instance, change) {
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
// Get the text from the start of the line to the cursor
const textToCursor = line.slice(0, cursor.ch);
// Do not autosuggest emojis in curly span/div/injector properties
if(line.includes('{')) {
const curlyToCursor = textToCursor.slice(textToCursor.indexOf(`{`));
const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g;
if(curlySpanRegex.test(curlyToCursor))
return;
}
// Check if the text ends with ':xyz'
if(/:[^\s:]+$/.test(textToCursor)) {
CodeMirror.commands.autocomplete(editor);
}
});
};
module.exports = {
showAutocompleteEmoji
};

View File

@@ -0,0 +1,48 @@
const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
const ranges = cm.listSelections(), replacements = [];
for (let i = 0; i < ranges.length; i++) {
if(!ranges[i].empty()) return CodeMirror.Pass;
const pos = ranges[i].head, line = cm.getLine(pos.line), tok = cm.getTokenAt(pos);
if(!typingClosingBrace && (tok.type == 'string' || tok.string.charAt(0) != '{' || tok.start != pos.ch - 1))
return CodeMirror.Pass;
else if(typingClosingBrace) {
let hasUnclosedBraces = false, index = -1;
do {
index = line.indexOf('{{', index + 1);
if(index !== -1 && line.indexOf('}}', index + 1) === -1) {
hasUnclosedBraces = true;
break;
}
} while (index !== -1);
if(!hasUnclosedBraces) return CodeMirror.Pass;
}
replacements[i] = typingClosingBrace ? {
text : '}}',
newPos : CodeMirror.Pos(pos.line, pos.ch + 2)
} : {
text : '{}}',
newPos : CodeMirror.Pos(pos.line, pos.ch + 1)
};
}
for (let i = ranges.length - 1; i >= 0; i--) {
const info = replacements[i];
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, '+insert');
const sel = cm.listSelections().slice(0);
sel[i] = {
head : info.newPos,
anchor : info.newPos
};
cm.setSelections(sel);
}
};
module.exports = {
autoCloseCurlyBraces : function(CodeMirror, codeMirror) {
const map = { name: 'autoCloseCurlyBraces' };
map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); };
map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); };
codeMirror.addKeyMap(map);
}
};

View File

@@ -0,0 +1,465 @@
/* eslint-disable max-lines */
require('./codeEditor.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const closeTag = require('./close-tag');
const autoCompleteEmoji = require('./autocompleteEmoji');
let CodeMirror;
if(typeof window !== 'undefined'){
CodeMirror = require('codemirror');
//Language Modes
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',
getDefaultProps : function() {
return {
language : '',
value : '',
wrap : true,
onChange : ()=>{},
enableFolding : true,
editorTheme : 'default'
};
},
getInitialState : function() {
return {
docs : {}
};
},
editor : React.createRef(null),
componentDidMount : function() {
this.buildEditor();
const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
this.codeMirror.swapDoc(newDoc);
},
componentDidUpdate : function(prevProps) {
if(prevProps.view !== this.props.view){ //view changed; swap documents
let newDoc;
if(!this.state.docs[this.props.view]) {
newDoc = CodeMirror.Doc(this.props.value, this.props.language);
} else {
newDoc = this.state.docs[this.props.view];
}
const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) };
this.setState((prevState)=>({
docs : _.merge({}, prevState.docs, oldDoc)
}));
this.props.rerenderParent();
} else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
this.codeMirror.setValue(this.props.value);
}
if(this.props.enableFolding) {
this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror));
} else {
this.codeMirror.setOption('foldOptions', false);
}
if(prevProps.editorTheme !== this.props.editorTheme){
this.codeMirror.setOption('theme', this.props.editorTheme);
}
},
buildEditor : function() {
this.codeMirror = CodeMirror(this.editor.current, {
lineNumbers : true,
lineWrapping : this.props.wrap,
indentWithTabs : false,
tabSize : 2,
smartIndent : false,
historyEventDelay : 250,
scrollPastEnd : true,
extraKeys : {
'Tab' : this.indent,
'Shift-Tab' : this.dedent,
'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold,
'Shift-Ctrl-=' : this.makeSuper,
'Shift-Cmd-=' : this.makeSuper,
'Ctrl-=' : this.makeSub,
'Cmd-=' : this.makeSub,
'Ctrl-I' : this.makeItalic,
'Cmd-I' : this.makeItalic,
'Ctrl-U' : this.makeUnderline,
'Cmd-U' : this.makeUnderline,
'Ctrl-.' : this.makeNbsp,
'Cmd-.' : this.makeNbsp,
'Shift-Ctrl-.' : this.makeSpace,
'Shift-Cmd-.' : this.makeSpace,
'Shift-Ctrl-,' : this.removeSpace,
'Shift-Cmd-,' : this.removeSpace,
'Ctrl-M' : this.makeSpan,
'Cmd-M' : this.makeSpan,
'Shift-Ctrl-M' : this.makeDiv,
'Shift-Cmd-M' : this.makeDiv,
'Ctrl-/' : this.makeComment,
'Cmd-/' : this.makeComment,
'Ctrl-K' : this.makeLink,
'Cmd-K' : this.makeLink,
'Ctrl-L' : ()=>this.makeList('UL'),
'Cmd-L' : ()=>this.makeList('UL'),
'Shift-Ctrl-L' : ()=>this.makeList('OL'),
'Shift-Cmd-L' : ()=>this.makeList('OL'),
'Shift-Ctrl-1' : ()=>this.makeHeader(1),
'Shift-Ctrl-2' : ()=>this.makeHeader(2),
'Shift-Ctrl-3' : ()=>this.makeHeader(3),
'Shift-Ctrl-4' : ()=>this.makeHeader(4),
'Shift-Ctrl-5' : ()=>this.makeHeader(5),
'Shift-Ctrl-6' : ()=>this.makeHeader(6),
'Shift-Cmd-1' : ()=>this.makeHeader(1),
'Shift-Cmd-2' : ()=>this.makeHeader(2),
'Shift-Cmd-3' : ()=>this.makeHeader(3),
'Shift-Cmd-4' : ()=>this.makeHeader(4),
'Shift-Cmd-5' : ()=>this.makeHeader(5),
'Shift-Cmd-6' : ()=>this.makeHeader(6),
'Shift-Ctrl-Enter' : this.newColumn,
'Shift-Cmd-Enter' : this.newColumn,
'Ctrl-Enter' : this.newPage,
'Cmd-Enter' : this.newPage,
'Ctrl-F' : 'findPersistent',
'Cmd-F' : 'findPersistent',
'Shift-Enter' : 'findPersistentPrevious',
'Ctrl-[' : this.foldAllCode,
'Cmd-[' : this.foldAllCode,
'Ctrl-]' : this.unfoldAllCode,
'Cmd-]' : this.unfoldAllCode
},
foldGutter : true,
foldOptions : this.foldOptions(this.codeMirror),
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseTags : true,
styleActiveLine : true,
showTrailingSpace : false,
theme : this.props.editorTheme
// specialChars : / /,
// specialCharPlaceholder : function(char) {
// const el = document.createElement('span');
// el.className = 'cm-space';
// el.innerHTML = ' ';
// return el;
// }
});
// Add custom behaviors (auto-close curlies and auto-complete emojis)
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror);
// 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.updateSize();
},
indent : function () {
const cm = this.codeMirror;
if(cm.somethingSelected()) {
cm.execCommand('indentMore');
} else {
cm.execCommand('insertSoftTab');
}
},
dedent : function () {
this.codeMirror.execCommand('indentLess');
},
makeHeader : function (number) {
const selection = this.codeMirror.getSelection();
const header = Array(number).fill('#').join('');
this.codeMirror.replaceSelection(`${header} ${selection}`, 'around');
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 });
},
makeBold : function() {
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){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
makeItalic : function() {
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
}
},
makeSuper : function() {
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^';
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
}
},
makeSub : function() {
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){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
makeNbsp : function() {
this.codeMirror.replaceSelection('&nbsp;', 'end');
},
makeSpace : function() {
const selection = this.codeMirror.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){
const percent = parseInt(selection.slice(8, -4)) + 10;
this.codeMirror.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around');
} else {
this.codeMirror.replaceSelection(`{{width:10% }}`, 'around');
}
},
removeSpace : function() {
const selection = this.codeMirror.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){
const percent = parseInt(selection.slice(8, -4)) - 10;
this.codeMirror.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around');
}
},
newColumn : function() {
this.codeMirror.replaceSelection('\n\\column\n\n', 'end');
},
newPage : function() {
this.codeMirror.replaceSelection('\n\\page\n\n', 'end');
},
injectText : function(injectText, overwrite=true) {
const cm = this.codeMirror;
if(!overwrite) {
cm.setCursor(cm.getCursor('from'));
}
cm.replaceSelection(injectText, 'end');
cm.focus();
},
makeUnderline : function() {
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');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
}
},
makeSpan : function() {
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){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
}
},
makeDiv : function() {
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');
if(selection.length === 0){
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
}
},
makeComment : function() {
let regex;
let cursorPos;
let newComment;
const selection = this.codeMirror.getSelection();
if(this.props.language === 'gfm'){
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
cursorPos = 4;
newComment = `<!-- ${selection} -->`;
} else {
regex = /^\s*(\/\*\s?)(.*?)(\s?\*\/)\s*$/gs;
cursorPos = 3;
newComment = `/* ${selection} */`;
}
this.codeMirror.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around');
if(selection.length === 0){
const cursor = this.codeMirror.getCursor();
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos });
};
},
makeLink : function() {
const isLink = /^\[(.*)\]\((.*)\)$/;
const selection = this.codeMirror.getSelection().trim();
let match;
if(match = isLink.exec(selection)){
const altText = match[1];
const url = match[2];
this.codeMirror.replaceSelection(`${altText} ${url}`);
const cursor = this.codeMirror.getCursor();
this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch });
} else {
this.codeMirror.replaceSelection(`[${selection || 'alt text'}](url)`);
const cursor = this.codeMirror.getCursor();
this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 });
}
},
makeList : function(listType) {
const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to');
this.codeMirror.setSelection(
{ line: selectionStart.line, ch: 0 },
{ line: selectionEnd.line, ch: this.codeMirror.getLine(selectionEnd.line).length }
);
const newSelection = this.codeMirror.getSelection();
const regex = /^\d+\.\s|^-\s/gm;
if(newSelection.match(regex) != null){ // if selection IS A LIST
this.codeMirror.replaceSelection(newSelection.replace(regex, ''), 'around');
} else { // if selection IS NOT A LIST
listType == 'UL' ? this.codeMirror.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') :
this.codeMirror.replaceSelection(newSelection.replace(/^/gm, (()=>{
let n = 1;
return ()=>{
return `${n++}. `;
};
})()), 'around');
}
},
foldAllCode : function() {
this.codeMirror.execCommand('foldAll');
},
unfoldAllCode : function() {
this.codeMirror.execCommand('unfoldAll');
},
//=-- Externally used -==//
setCursorPosition : function(line, char){
setTimeout(()=>{
this.codeMirror.focus();
this.codeMirror.doc.setCursor(line, char);
}, 10);
},
getCursorPosition : function(){
return this.codeMirror.getCursor();
},
getTopVisibleLine : function(){
const rect = this.codeMirror.getWrapperElement().getBoundingClientRect();
const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window');
return topVisibleLine;
},
updateSize : function(){
this.codeMirror.refresh();
},
redo : function(){
return this.codeMirror.redo();
},
undo : function(){
return this.codeMirror.undo();
},
historySize : function(){
return this.codeMirror.doc.historySize();
},
foldOptions : function(cm){
return {
scanUp : true,
rangeFinder : this.props.language === 'css' ? CodeMirror.fold.homebrewerycss : CodeMirror.fold.homebrewery,
widget : (from, to)=>{
let text = '';
let currentLine = from.line;
let maxLength = 50;
let foldPreviewText = '';
while (currentLine <= to.line && text.length <= maxLength) {
const currentText = this.codeMirror.getLine(currentLine);
currentLine++;
if(currentText[0] == '#'){
foldPreviewText = currentText;
break;
}
if(!foldPreviewText && currentText != '\n') {
foldPreviewText = currentText;
}
}
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
text = text.replace('{', '').trim();
// Truncate data URLs at `data:`
const startOfData = text.indexOf('data:');
if(startOfData > 0)
maxLength = Math.min(startOfData + 5, maxLength);
if(text.length > maxLength)
text = `${text.slice(0, maxLength)}...`;
return `\u21A4 ${text} \u21A6`;
}
};
},
//----------------------//
render : function(){
return <>
<link href={`../homebrew/cm-themes/${this.props.editorTheme}.css`} type='text/css' rel='stylesheet' />
<div className='codeEditor' ref={this.editor} style={this.props.style}/>
</>;
}
});
module.exports = CodeEditor;

View File

@@ -0,0 +1,60 @@
@import (less) 'codemirror/lib/codemirror.css';
@import (less) 'codemirror/addon/fold/foldgutter.css';
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
@import (less) 'codemirror/addon/dialog/dialog.css';
@import (less) 'codemirror/addon/hint/show-hint.css';
//Icon fonts included so they can appear in emoji autosuggest dropdown
@import (less) './themes/fonts/iconFonts/diceFont.less';
@import (less) './themes/fonts/iconFonts/elderberryInn.less';
@import (less) './themes/fonts/iconFonts/gameIcons.less';
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
@keyframes sourceMoveAnimation {
50% { color : white;background-color : red;}
100% { color : unset;background-color : unset;}
}
.codeEditor {
@media screen and (pointer : coarse) {
font-size : 16px;
}
.CodeMirror-foldmarker {
font-family : inherit;
font-weight : 600;
color : grey;
text-shadow : none;
}
.CodeMirror-foldgutter {
cursor : pointer;
border-left : 1px solid #EEEEEE;
transition : background 0.1s;
&:hover { background : #DDDDDD; }
}
.sourceMoveFlash .CodeMirror-line {
animation-name : sourceMoveAnimation;
animation-duration : 0.4s;
}
.CodeMirror-search-field {
width:25em !important;
outline:1px inset #00000055 !important;
}
//.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
//}
//.cm-trailingspace {
// .cm-space {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
// }
//}
}
.emojiPreview {
font-size : 1.5em;
line-height : 1.2em;
}

View File

@@ -0,0 +1,44 @@
module.exports = {
registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) {
// BRACE FOLDING
const startMatcher = /\{[ \t]*$/;
const endMatcher = /\}[ \t]*$/;
const activeLine = cm.getLine(start.line);
if(activeLine.match(startMatcher)) {
const lastLineNo = cm.lastLine();
let end = start.line + 1;
let braceCount = 1;
while (end < lastLineNo) {
const curLine = cm.getLine(end);
if(curLine.match(startMatcher)) braceCount++;
if(curLine.match(endMatcher)) braceCount--;
if(braceCount == 0) break;
++end;
}
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(end, cm.getLine(end).length)
};
}
// @import and data-url folding
const importMatcher = /^@import.*?;/;
const dataURLMatcher = /url\(.*?data\:.*\)/;
if(activeLine.match(importMatcher) || activeLine.match(dataURLMatcher)) {
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(start.line, activeLine.length)
};
}
return null;
});
}
};

View File

@@ -0,0 +1,26 @@
module.exports = {
registerHomebreweryHelper : function(CodeMirror) {
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
const matcher = /^\\page.*/;
const prevLine = cm.getLine(start.line - 1);
if(start.line === cm.firstLine() || prevLine.match(matcher)) {
const lastLineNo = cm.lastLine();
let end = start.line;
while (end < lastLineNo) {
if(cm.getLine(end + 1).match(matcher))
break;
++end;
}
return {
from : CodeMirror.Pos(start.line, 0),
to : CodeMirror.Pos(end, cm.getLine(end).length)
};
}
return null;
});
}
};

View File

@@ -0,0 +1,60 @@
require('./renderWarnings.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
import Dialog from '../dialog.jsx';
const RenderWarnings = createClass({
displayName : 'RenderWarnings',
getInitialState : function() {
return {
warnings : {}
};
},
componentDidMount : function() {
this.checkWarnings();
window.addEventListener('resize', this.checkWarnings);
},
componentWillUnmount : function() {
window.removeEventListener('resize', this.checkWarnings);
},
warnings : {
chrome : function(){
const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
if(!isChrome){
return <li key='chrome'>
<em>Built for Chrome </em> <br />
Other browsers have not been tested for compatibility. If you
experience issues with your document not rendering or printing
properly, please try using the latest version of Chrome before
submitting a bug report.
</li>;
}
},
},
checkWarnings : function(){
this.setState({
warnings : _.reduce(this.warnings, (r, fn, type)=>{
const element = fn();
if(element) r[type] = element;
return r;
}, {})
});
},
render : function(){
if(_.isEmpty(this.state.warnings)) return null;
const DISMISS_KEY = 'dismiss_render_warning';
const DISMISS_TEXT = <i className='fas fa-times dismiss' />;
return <Dialog className='renderWarnings' dismissKey={DISMISS_KEY} closeText={DISMISS_TEXT}>
<i className='fas fa-exclamation-triangle ohno' />
<h3>Render Warnings</h3>
<small>If this homebrew is rendering badly if might be because of the following:</small>
<ul>{_.values(this.state.warnings)}</ul>
</Dialog>;
}
});
module.exports = RenderWarnings;

View File

@@ -0,0 +1,48 @@
.renderWarnings {
position : relative;
float : right;
width : 350px;
padding : 20px;
padding-bottom : 10px;
padding-left : 85px;
margin-bottom : 10px;
color : white;
background-color : @yellow;
border : none;
a { font-weight : 800; }
i.ohno {
position : absolute;
top : 24px;
left : 24px;
font-size : 2.5em;
opacity : 0.8;
}
button.dismiss {
position : absolute;
top : 10px;
right : 10px;
cursor : pointer;
background-color : transparent;
opacity : 0.6;
&:hover { opacity : 1; }
}
small {
font-size : 0.6em;
opacity : 0.7;
}
h3 {
font-size : 1.1em;
font-weight : 800;
}
ul {
margin-top : 15px;
font-size : 0.8em;
list-style-position : outside;
list-style-type : disc;
li {
font-size : 0.8em;
line-height : 1.6em;
em { font-weight : 800; }
}
}
}

View File

@@ -2,7 +2,8 @@ require('./splitPane.less');
const React = require('react');
const { useState, useEffect } = React;
const storageKey = 'naturalcrit-pane-split';
const PANE_WIDTH_KEY = 'HB_editor_splitWidth';
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
const SplitPane = (props)=>{
const {
@@ -18,9 +19,9 @@ const SplitPane = (props)=>{
const [liveScroll, setLiveScroll] = useState(false);
useEffect(()=>{
const savedPos = window.localStorage.getItem(storageKey);
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
setLiveScroll(window.localStorage.getItem('liveScroll') === 'true');
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
window.addEventListener('resize', handleResize);
return ()=>window.removeEventListener('resize', handleResize);
@@ -29,13 +30,13 @@ const SplitPane = (props)=>{
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleUp =(e)=>{
e.preventDefault();
if(isDragging) {
onDragFinish(dividerPos);
window.localStorage.setItem(storageKey, dividerPos);
window.localStorage.setItem(PANE_WIDTH_KEY, dividerPos);
}
setIsDragging(false);
};
@@ -52,7 +53,7 @@ const SplitPane = (props)=>{
};
const liveScrollToggle = ()=>{
window.localStorage.setItem('liveScroll', String(!liveScroll));
window.localStorage.setItem(LIVE_SCROLL_KEY, String(!liveScroll));
setLiveScroll(!liveScroll);
};

View File

@@ -0,0 +1,10 @@
const React = require('react');
const createClass = require('create-react-class');
module.exports = function(props){
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'/>
<circle cx='62.905' cy='10.089' r='3.595'/>
<circle cx='52.616' cy='5.048' r='2.73'/>
</svg>;
};

View File

@@ -0,0 +1,6 @@
const React = require('react');
const createClass = require('create-react-class');
module.exports = 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>;
};

View File

@@ -4,13 +4,13 @@ const React = require('react');
const { useState, useRef, useMemo, useEffect } = React;
const _ = require('lodash');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
import Markdown from 'naturalcrit/markdown.js';
const MarkdownLegacy = require('markdownLegacy.js');
import Markdown from 'markdown.js';
const ErrorBar = require('./errorBar/errorBar.jsx');
const ToolBar = require('./toolBar/toolBar.jsx');
//TODO: move to the brew renderer
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
const RenderWarnings = require('client/components/renderWarnings/renderWarnings.jsx');
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
const Frame = require('react-frame-component').default;
const dedent = require('dedent-tabs').default;
@@ -24,6 +24,8 @@ const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056;
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
@@ -122,7 +124,7 @@ const BrewRenderer = (props)=>{
//useEffect to store or gather toolbar state from storage
useEffect(()=>{
const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY));
toolbarState && setDisplayOptions(toolbarState);
}, []);
@@ -284,7 +286,7 @@ const BrewRenderer = (props)=>{
const handleDisplayOptionsChange = (newDisplayOptions)=>{
setDisplayOptions(newDisplayOptions);
localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions));
};
const pagesStyle = {
@@ -293,12 +295,6 @@ const BrewRenderer = (props)=>{
rowGap : `${displayOptions.rowGap}px`
};
const styleObject = {};
if(global.config.deployment) {
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
}
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
@@ -327,10 +323,9 @@ const BrewRenderer = (props)=>{
contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
>
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
<div className='brewRenderer'
onKeyDown={handleControlKeys}
tabIndex={-1}
style={ styleObject }
>
{/* Apply CSS from Style tab and render pages from Markdown tab */}

View File

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

View File

@@ -1,7 +1,7 @@
require('./notificationPopup.less');
import React, { useEffect, useState } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import Markdown from 'markdown.js';
import Dialog from '../../../components/dialog.jsx';

View File

@@ -9,6 +9,8 @@ import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anch
const MAX_ZOOM = 300;
const MIN_ZOOM = 10;
const TOOLBAR_VISIBILITY = 'HB_renderer_toolbarVisibility';
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
const [pageNum, setPageNum] = useState(1);
@@ -21,8 +23,8 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
}, [visiblePages]);
useEffect(()=>{
const Visibility = localStorage.getItem('hb_toolbarVisibility');
if (Visibility) setToolsVisible(Visibility === 'true');
const Visibility = localStorage.getItem(TOOLBAR_VISIBILITY);
if(Visibility) setToolsVisible(Visibility === 'true');
}, []);
@@ -100,7 +102,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
setToolsVisible(!toolsVisible);
localStorage.setItem('hb_toolbarVisibility', !toolsVisible);
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible);
}}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div>

View File

@@ -4,13 +4,13 @@ const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const dedent = require('dedent-tabs').default;
import Markdown from '../../../shared/naturalcrit/markdown.js';
import Markdown from '../../../shared/markdown.js';
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const CodeEditor = require('client/components/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const EDITOR_THEME_KEY = 'HB_editor_theme';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -40,11 +40,8 @@ const Editor = createClass({
style : ''
},
onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{},
onBrewChange : ()=>{},
reportError : ()=>{},
onCursorPageChange : ()=>{},
onViewPageChange : ()=>{},
@@ -143,7 +140,7 @@ const Editor = createClass({
handleViewChange : function(newView){
this.props.setMoveArrows(newView === 'text');
this.setState({
view : newView
}, ()=>{
@@ -328,10 +325,10 @@ const Editor = createClass({
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
const checkIfScrollComplete = ()=>{
let scrollingTimeout;
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
@@ -372,8 +369,8 @@ const Editor = createClass({
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
const checkIfScrollComplete = ()=>{
let scrollingTimeout;
let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
scrollingTimeout = setTimeout(()=>{
isJumping = false;
@@ -412,7 +409,6 @@ const Editor = createClass({
//Called when there are changes to the editor's dimensions
update : function(){
this.codeEditor.current?.updateSize();
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
if(snipHeight !== this.state.snippetbarHeight)
this.setState({ snippetbarHeight: snipHeight });
@@ -438,7 +434,7 @@ const Editor = createClass({
language='gfm'
view={this.state.view}
value={this.props.brew.text}
onChange={this.props.onTextChange}
onChange={this.props.onBrewChange('text')}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
@@ -451,7 +447,7 @@ const Editor = createClass({
language='css'
view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange}
onChange={this.props.onBrewChange('style')}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
@@ -467,7 +463,7 @@ const Editor = createClass({
<MetadataEditor
metadata={this.props.brew}
themeBundle={this.props.themeBundle}
onChange={this.props.onMetaChange}
onChange={this.props.onBrewChange('metadata')}
reportError={this.props.reportError}
userThemes={this.props.userThemes}/>
</>;
@@ -481,7 +477,7 @@ const Editor = createClass({
language='gfm'
view={this.state.view}
value={this.props.brew.snippets}
onChange={this.props.onSnipChange}
onChange={this.props.onBrewChange('snippets')}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}

View File

@@ -207,8 +207,6 @@ const MetadataEditor = createClass({
},
renderThemeDropdown : function(){
if(!global.enable_themes) return;
const mergedThemes = _.merge(Themes, this.props.userThemes);
const listThemes = (renderer)=>{
@@ -307,8 +305,6 @@ const MetadataEditor = createClass({
},
renderRenderOptions : function(){
if(!global.enable_v3) return;
return <div className='field systems'>
<label>Renderer</label>
<div className='value'>

View File

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

View File

@@ -4,6 +4,8 @@ import './homebrew.less';
import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
import HomePage from './pages/homePage/homePage.jsx';
import EditPage from './pages/editPage/editPage.jsx';
import UserPage from './pages/userPage/userPage.jsx';
@@ -17,7 +19,6 @@ const WithRoute = ({ el: Element, ...rest })=>{
const params = useParams();
const [searchParams] = useSearchParams();
const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />;
};
@@ -26,8 +27,6 @@ const Homebrew = (props)=>{
url = '',
version = '0.0.0',
account = null,
enable_v3 = false,
enable_themes,
config,
brew = {
title : '',
@@ -44,13 +43,22 @@ const Homebrew = (props)=>{
global.account = account;
global.version = version;
global.enable_v3 = enable_v3;
global.enable_themes = enable_themes;
global.config = config;
const backgroundObject = ()=>{
if(global.config.deployment || (config.local && config.development)){
const bgText = global.config.deployment || 'Local';
return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
};
}
return null;
};
updateLocalStorage();
return (
<Router location={url}>
<div className='homebrew'>
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />

View File

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

View File

@@ -1,6 +1,6 @@
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const request = require('superagent');
const Account = createClass({

View File

@@ -1,10 +1,10 @@
require('./error-navitem.less');
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const ErrorNavItem = ({error = '', clearError})=>{
const ErrorNavItem = ({ error = '', clearError })=>{
const response = error.response;
const errorCode = error.code
const errorCode = error.code;
const status = response?.status;
const HBErrorCode = response?.body?.HBErrorCode;
const message = response?.body?.message;
@@ -15,7 +15,7 @@ const ErrorNavItem = ({error = '', clearError})=>{
errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
} catch {}
if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
@@ -112,6 +112,15 @@ const ErrorNavItem = ({error = '', clearError})=>{
</Nav.item>;
}
if(HBErrorCode === '13') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Server has lost connection to the database.
</div>
</Nav.item>;
}
if(errorCode === 'ECONNABORTED') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!

View File

@@ -1,7 +1,7 @@
const React = require('react');
const dedent = require('dedent-tabs').default;
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
module.exports = function(props){
return <Nav.dropdown>

View File

@@ -2,7 +2,7 @@ const React = require('react');
const createClass = require('create-react-class');
const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const MetadataNav = createClass({

View File

@@ -0,0 +1,120 @@
require('client/homebrew/navbar/navbar.less');
const React = require('react');
const { useState, useRef, useEffect } = React;
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const NaturalCritIcon = require('client/components/svg/naturalcrit-d20.svg.jsx');
const Nav = {
base : createClass({
displayName : 'Nav.base',
render : function(){
return <nav>
{this.props.children}
</nav>;
}
}),
logo : function(){
return <a className='navLogo' href='https://www.naturalcrit.com'>
<NaturalCritIcon />
<span className='name'>
Natural<span className='crit'>Crit</span>
</span>
</a>;
},
section : createClass({
displayName : 'Nav.section',
render : function(){
return <div className={`navSection ${this.props.className ?? ''}`}>
{this.props.children}
</div>;
}
}),
item : createClass({
displayName : 'Nav.item',
getDefaultProps : function() {
return {
icon : null,
href : null,
newTab : false,
onClick : function(){},
color : null
};
},
handleClick : function(e){
this.props.onClick(e);
},
render : function(){
const classes = cx('navItem', this.props.color, this.props.className);
let icon;
if(this.props.icon) icon = <i className={this.props.icon} />;
const props = _.omit(this.props, ['newTab']);
if(this.props.href){
return <a {...props} className={classes} target={this.props.newTab ? '_blank' : '_self'} >
{this.props.children}
{icon}
</a>;
} else {
return <div {...props} className={classes} onClick={this.handleClick} >
{this.props.children}
{icon}
</div>;
}
}
}),
dropdown : function dropdown(props) {
props = Object.assign({}, props, {
trigger : 'hover click'
});
const myRef = useRef(null);
const [showDropdown, setShowDropdown] = useState(false);
useEffect(()=>{
document.addEventListener('click', handleClickOutside);
return ()=>{
document.removeEventListener('click', handleClickOutside);
};
}, []);
function handleClickOutside(e) {
// Close dropdown when clicked outside
if(!myRef.current?.contains(e.target)) {
handleDropdown(false);
}
}
function handleDropdown(show) {
setShowDropdown(show ?? !showDropdown);
}
const dropdownChildren = React.Children.map(props.children, (child, i)=>{
if(i < 1) return;
return child;
});
return (
<div className={`navDropdownContainer ${props.className ?? ''}`}
ref={myRef}
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
onClick = { props.trigger.includes('click') ? ()=>handleDropdown(true) : undefined }
>
{props.children[0] || props.children /*children is not an array when only one child*/}
{showDropdown && <div className='navDropdown'>{dropdownChildren}</div>}
</div>
);
}
};
module.exports = Nav;

View File

@@ -2,7 +2,7 @@ require('./navbar.less');
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const PatreonNavItem = require('./patreon.navitem.jsx');
const Navbar = createClass({

View File

@@ -37,7 +37,10 @@
&:has(.brewTitle) {
flex-grow : 1;
min-width : 300px;
min-width : 300px;
}
>.brewTitle {
cursor:auto;
}
}
// "NaturalCrit" logo

View File

@@ -1,6 +1,6 @@
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
const BREWKEY = 'homebrewery-new';
@@ -34,8 +34,8 @@ const NewBrew = ()=>{
}
const type = file.name.split('.').pop().toLowerCase();
alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file);

View File

@@ -1,5 +1,5 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
module.exports = function(props){
return <Nav.item

View File

@@ -1,5 +1,5 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const { printCurrentBrew } = require('../../../shared/helpers.js');
module.exports = function(){

View File

@@ -3,10 +3,10 @@ const createClass = require('create-react-class');
const _ = require('lodash');
const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const EDIT_KEY = 'homebrewery-recently-edited';
const VIEW_KEY = 'homebrewery-recently-viewed';
const EDIT_KEY = 'HB_nav_recentlyEdited';
const VIEW_KEY = 'HB_nav_recentlyViewed';
const RecentItems = createClass({

View File

@@ -1,23 +1,23 @@
import React from 'react';
import dedent from 'dedent-tabs';
import Nav from 'naturalcrit/nav/nav.jsx';
import Nav from 'client/homebrew/navbar/nav.jsx';
const getShareId = (brew)=>(
brew.googleId && !brew.stubbed
? brew.googleId + brew.shareId
: brew.shareId
);
const getShareId = (brew)=>(
brew.googleId && !brew.stubbed
? brew.googleId + brew.shareId
: brew.shareId
);
const getRedditLink = (brew)=>{
const text = dedent`
const getRedditLink = (brew)=>{
const text = dedent`
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
export default ({brew}) => (
export default ({ brew })=>(
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share

View File

@@ -1,6 +1,6 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
module.exports = function (props) {
return (

View File

@@ -1,7 +1,7 @@
const React = require('react');
const moment = require('moment');
const UIPage = require('../basePages/uiPage/uiPage.jsx');
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
const NaturalCritIcon = require('client/components/svg/naturalcrit-d20.svg.jsx');
let SAVEKEY = '';
@@ -13,7 +13,7 @@ const AccountPage = (props)=>{
// initialize save location from local storage based on user id
React.useEffect(()=>{
if(!saveLocation && accountDetails.username) {
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
SAVEKEY = `HB_editor_defaultSave_${accountDetails.username}`;
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');

View File

@@ -7,7 +7,9 @@ const moment = require('moment');
const BrewItem = require('./brewItem/brewItem.jsx');
const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir';
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
const DEFAULT_SORT_TYPE = 'alpha';
const DEFAULT_SORT_DIR = 'asc';
@@ -50,12 +52,12 @@ const ListPage = createClass({
// LOAD FROM LOCAL STORAGE
if(typeof window !== 'undefined') {
const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
const newSortType = (this.state.sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE));
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR));
this.updateUrl(this.state.filterString, newSortType, newSortDir);
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
brewGroup.visible = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true')=='true';
return brewGroup;
});
@@ -73,10 +75,10 @@ const ListPage = createClass({
saveToLocalStorage : function() {
this.state.brewCollection.map((brewGroup)=>{
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${brewGroup.visible}`);
});
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
localStorage.setItem(USERPAGE_SORT_TYPE, this.state.sortType);
localStorage.setItem(USERPAGE_SORT_DIR, this.state.sortDir);
},
renderBrews : function(brews){

View File

@@ -2,12 +2,12 @@ require('./uiPage.less');
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../../navbar/navbar.jsx');
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
const Account = require('../../../navbar/account.navitem.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const Navbar = require('client/homebrew/navbar/navbar.jsx');
const NewBrewItem = require('client/homebrew/navbar/newbrew.navitem.jsx');
const HelpNavItem = require('client/homebrew/navbar/help.navitem.jsx');
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both;
const Account = require('client/homebrew/navbar/account.navitem.jsx');
const UIPage = createClass({

View File

@@ -1,44 +1,54 @@
/* eslint-disable max-lines */
import './editPage.less';
import React, { useState, useEffect, useRef, useCallback } from 'react';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import Markdown from 'markdown.js';
import _ from 'lodash';
import _ from 'lodash';;
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import { Meta } from 'vitreum/headtags';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ShareNavItem from '../../navbar/share.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'client/homebrew/navbar/nav.jsx';
import Navbar from 'client/homebrew/navbar/navbar.jsx';
import NewBrewItem from 'client/homebrew/navbar/newbrew.navitem.jsx';
import AccountNavItem from 'client/homebrew/navbar/account.navitem.jsx';
import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx';
import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx';
import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx';
import PrintNavItem from 'client/homebrew/navbar/print.navitem.jsx';
import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx';
// Page specific imports
import { Meta } from 'vitreum/headtags';
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import ShareNavItem from 'client/homebrew/navbar/share.navitem.jsx';
import LockNotification from './lockNotification/lockNotification.jsx';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js';
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg';
const SAVE_TIMEOUT = 10000;
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const SNIPKEY = 'HB_newPage_snippets';
const METAKEY = 'HB_newPage_meta';
const useLocalStorage = false;
const neverSaved = false;
const EditPage = (props)=>{
props = {
brew : DEFAULT_BREW_LOAD,
@@ -70,7 +80,7 @@ const EditPage = (props)=>{
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true);
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
setAutoSaveEnabled(autoSavePref);
setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(Markdown.validate(currentBrew.text));
@@ -109,45 +119,35 @@ const EditPage = (props)=>{
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
useEffect(()=>{
trySave(true);
}, [saveGoogle]);
const handleSplitMove = ()=>{
editorRef.current?.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
const handleTextChange = (text)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length)
setHTMLErrors(Markdown.validate(text));
setCurrentBrew((prevBrew)=>({ ...prevBrew, text }));
};
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
const handleStyleChange = (style)=>{
setCurrentBrew((prevBrew)=>({ ...prevBrew, style }));
};
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
const handleSnipChange = (snippet)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length)
setHTMLErrors(Markdown.validate(snippet));
setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet }));
};
const handleMetaChange = (metadata, field = undefined)=>{
if(field === 'theme' || field === 'renderer')
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setCurrentBrew((prev)=>({ ...prev, ...metadata }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
@@ -183,7 +183,6 @@ const EditPage = (props)=>{
const toggleGoogleStorage = ()=>{
setSaveGoogle((prev)=>!prev);
setError(null);
trySave(true);
};
const trySave = (immediate = false, hasChanges = true)=>{
@@ -313,20 +312,24 @@ const EditPage = (props)=>{
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved.</Nav.item>;
return <Nav.item className='save saved'>saved</Nav.item>;
};
const toggleAutoSave = ()=>{
clearTimeout(warnUnsavedTimeout.current);
clearTimeout(saveTimeout.current);
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled));
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
setAutoSaveEnabled(!autoSaveEnabled);
setWarnUnsavedChanges(autoSaveEnabled);
};
@@ -356,11 +359,11 @@ const EditPage = (props)=>{
{renderSaveButton()}
{renderAutoSaveButton()}
</Nav.dropdown>}
<NewBrewItem/>
<HelpNavItem/>
<ShareNavItem brew={currentBrew} />
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<ShareNavItem brew={currentBrew} />
<RecentNavItem brew={currentBrew} storageKey='edit' />
<AccountNavItem/>
</Nav.section>
@@ -380,17 +383,14 @@ const EditPage = (props)=>{
<Editor
ref={editorRef}
brew={currentBrew}
onTextChange={handleTextChange}
onStyleChange={handleStyleChange}
onSnipChange={handleSnipChange}
onMetaChange={handleMetaChange}
onBrewChange={handleBrewChange}
reportError={setError}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -403,7 +403,7 @@ const EditPage = (props)=>{
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={handleBrewRendererPageChange}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}

View File

@@ -1,7 +1,7 @@
require('./errorPage.less');
const React = require('react');
const UIPage = require('../basePages/uiPage/uiPage.jsx');
import Markdown from '../../../../shared/naturalcrit/markdown.js';
import Markdown from '../../../../shared/markdown.js';
const ErrorIndex = require('./errors/errorIndex.js');
const ErrorPage = ({ brew })=>{

View File

@@ -196,6 +196,12 @@ const errorIndex = (props)=>{
**Brew ID:** ${props.brew.brewId}`,
// Database Connection Lost
'13' : dedent`
## Database connection has been lost.
The server could not communicate with the database.`,
//account page when account is not defined
'50' : dedent`
## You are not signed in

View File

@@ -1,51 +1,84 @@
/* eslint-disable max-lines */
import './homePage.less';
import React from 'react';
import { useEffect, useState, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import { Meta } from 'vitreum/headtags';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'markdown.js';
import _ from 'lodash';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import { fetchThemeBundle } from '../../../../shared/helpers.js';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import Nav from 'client/homebrew/navbar/nav.jsx';
import Navbar from 'client/homebrew/navbar/navbar.jsx';
import NewBrewItem from 'client/homebrew/navbar/newbrew.navitem.jsx';
import AccountNavItem from 'client/homebrew/navbar/account.navitem.jsx';
import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx';
import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx';
import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx';
import PrintNavItem from 'client/homebrew/navbar/print.navitem.jsx';
import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx';
// Page specific imports
import { Meta } from 'vitreum/headtags';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const useLocalStorage = false;
const neverSaved = true;
const HomePage =(props)=>{
props = {
brew : DEFAULT_BREW,
ver : '0.0.0',
...props
};
...props
};
const [brew , setBrew] = useState(props.brew);
const [welcomeText , setWelcomeText] = useState(props.brew.text);
const [currentBrew , setCurrentBrew] = useState(props.brew);
const [error , setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = useState(false);
const [isSaving , setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
const editorRef = useRef(null);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme);
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('keydown', handleControlKeys);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
const save = ()=>{
request.post('/api')
.send(brew)
.send(currentBrew)
.end((err, res)=>{
if(err) {
setError(err);
@@ -56,24 +89,73 @@ const HomePage =(props)=>{
});
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
const handleTextChange = (text)=>{
setBrew((prevBrew) => ({ ...prevBrew, text }));
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
// if(unsavedChanges && warnUnsavedChanges) {
// resetWarnUnsavedTimer();
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
// const text = elapsedTime === 0
// ? 'Autosave is OFF.'
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{
@@ -84,11 +166,11 @@ const HomePage =(props)=>{
const renderNavbar = ()=>{
return <Navbar ver={props.ver}>
<Nav.section>
{error ?
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
null
}
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
@@ -105,22 +187,22 @@ const HomePage =(props)=>{
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={brew}
onTextChange={handleTextChange}
renderer={brew.renderer}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
showEditButtons={false}
themeBundle={themeBundle}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={brew.text}
style={brew.style}
renderer={brew.renderer}
onPageChange={handleBrewRendererPageChange}
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -128,7 +210,7 @@ const HomePage =(props)=>{
/>
</SplitPane>
</div>
<div className={`floatingSaveButton${welcomeText !== brew.text ? ' show' : ''}`} onClick={save}>
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
Save current <i className='fas fa-save' />
</div>
@@ -136,7 +218,7 @@ const HomePage =(props)=>{
Create your own <i className='fas fa-magic' />
</a>
</div>
)
);
};
module.exports = HomePage;

View File

@@ -35,6 +35,14 @@
.navItem.save {
background-color : @orange;
transition:all 0.2s;
&:hover { background-color : @green; }
&.neverSaved {
translate:-100%;
opacity: 0;
background-color :#333;
cursor:auto;
}
}
}

View File

@@ -1,33 +1,44 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
/* eslint-disable max-lines */
import './newPage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import Markdown from 'markdown.js';
import _ from 'lodash';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import Nav from 'client/homebrew/navbar/nav.jsx';
import Navbar from 'client/homebrew/navbar/navbar.jsx';
import NewBrewItem from 'client/homebrew/navbar/newbrew.navitem.jsx';
import AccountNavItem from 'client/homebrew/navbar/account.navitem.jsx';
import ErrorNavItem from 'client/homebrew/navbar/error-navitem.jsx';
import HelpNavItem from 'client/homebrew/navbar/help.navitem.jsx';
import VaultNavItem from 'client/homebrew/navbar/vault.navitem.jsx';
import PrintNavItem from 'client/homebrew/navbar/print.navitem.jsx';
import { both as RecentNavItem } from 'client/homebrew/navbar/recent.navitem.jsx';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
// Page specific imports
import { Meta } from 'vitreum/headtags';
const NewPage = (props) => {
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const METAKEY = 'HB_newPage_metadata';
const SNIPKEY = 'HB_newPage_snippets';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true;
const neverSaved = true;
const NewPage = (props)=>{
props = {
brew: DEFAULT_BREW,
brew : DEFAULT_BREW,
...props
};
@@ -40,15 +51,33 @@ const NewPage = (props) => {
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
const editorRef = useRef(null);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
// const saveTimeout = 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 unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(() => {
document.addEventListener('keydown', handleControlKeys);
useEffect(()=>{
loadBrew();
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
return () => {
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('keydown', handleControlKeys);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
@@ -67,9 +96,11 @@ const NewPage = (props) => {
brew.lang = metaStorage?.lang ?? brew.lang;
}
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew);
lastSavedBrew.current = brew;
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
localStorage.setItem(BREWKEY, brew.text);
@@ -80,74 +111,49 @@ const NewPage = (props) => {
window.history.replaceState({}, window.location.title, '/new/');
};
const handleControlKeys = (e) => {
if (!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if (e.keyCode === S_KEY) save();
if (e.keyCode === P_KEY) printCurrentBrew();
if (e.keyCode === S_KEY || e.keyCode === P_KEY) {
e.preventDefault();
e.stopPropagation();
}
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
useEffect(()=>{
trySaveRef.current = trySave;
unsavedChangesRef.current = unsavedChanges;
});
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
const handleTextChange = (text)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length)
HTMLErrors = Markdown.validate(text);
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew) => ({ ...prevBrew, text }));
localStorage.setItem(BREWKEY, text);
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
const handleStyleChange = (style) => {
setCurrentBrew(prevBrew => ({ ...prevBrew, style }));
localStorage.setItem(STYLEKEY, style);
};
const handleSnipChange = (snippet)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length)
HTMLErrors = Markdown.validate(snippet);
setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet }));
};
const handleMetaChange = (metadata, field = undefined) => {
if (field === 'theme' || field === 'renderer')
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setCurrentBrew(prev => ({ ...prev, ...metadata }));
localStorage.setItem(METAKEY, JSON.stringify({
renderer : metadata.renderer,
theme : metadata.theme,
lang : metadata.lang
}));
};
const save = async () => {
const trySave = async ()=>{
setIsSaving(true);
let updatedBrew = { ...currentBrew };
const updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
@@ -156,13 +162,13 @@ const NewPage = (props) => {
const res = await request
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(updatedBrew)
.catch((err) => {
.catch((err)=>{
setIsSaving(false);
setError(err);
});
setIsSaving(false)
if (!res) return;
setIsSaving(false);
if(!res) return;
const savedBrew = res.body;
@@ -173,15 +179,38 @@ const NewPage = (props) => {
};
const renderSaveButton = ()=>{
if(isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
save
</Nav.item>;
}
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
// if(unsavedChanges && warnUnsavedChanges) {
// resetWarnUnsavedTimer();
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
// const text = elapsedTime === 0
// ? 'Autosave is OFF.'
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={trySave} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{
@@ -189,7 +218,7 @@ const NewPage = (props) => {
setIsSaving(false);
};
const renderNavbar = () => (
const renderNavbar = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
@@ -199,8 +228,10 @@ const NewPage = (props) => {
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
@@ -208,22 +239,19 @@ const NewPage = (props) => {
);
return (
<div className='newPage sitePage'>
<div className='newPage sitePage'>
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onTextChange={handleTextChange}
onStyleChange={handleStyleChange}
onMetaChange={handleMetaChange}
onSnipChange={handleSnipChange}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -236,7 +264,7 @@ const NewPage = (props) => {
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={handleBrewRendererPageChange}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}

View File

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

View File

@@ -3,12 +3,12 @@ const React = require('react');
const { useState, useEffect, useCallback } = React;
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
const PrintNavItem = require('../../navbar/print.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const Navbar = require('client/homebrew/navbar/navbar.jsx');
const MetadataNav = require('client/homebrew/navbar/metadata.navitem.jsx');
const PrintNavItem = require('client/homebrew/navbar/print.navitem.jsx');
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both;
const Account = require('client/homebrew/navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');

View File

@@ -4,14 +4,14 @@ const _ = require('lodash');
const ListPage = require('../basePages/listPage/listPage.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const VaultNavitem = require('../../navbar/vault.navitem.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const Navbar = require('client/homebrew/navbar/navbar.jsx');
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both;
const Account = require('client/homebrew/navbar/account.navitem.jsx');
const NewBrew = require('client/homebrew/navbar/newbrew.navitem.jsx');
const HelpNavItem = require('client/homebrew/navbar/help.navitem.jsx');
const ErrorNavItem = require('client/homebrew/navbar/error-navitem.jsx');
const VaultNavitem = require('client/homebrew/navbar/vault.navitem.jsx');
const UserPage = (props)=>{
props = {

View File

@@ -5,12 +5,12 @@ require('./vaultPage.less');
const React = require('react');
const { useState, useEffect, useRef } = React;
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const Nav = require('client/homebrew/navbar/nav.jsx');
const Navbar = require('client/homebrew/navbar/navbar.jsx');
const RecentNavItem = require('client/homebrew/navbar/recent.navitem.jsx').both;
const Account = require('client/homebrew/navbar/account.navitem.jsx');
const NewBrew = require('client/homebrew/navbar/newbrew.navitem.jsx');
const HelpNavItem = require('client/homebrew/navbar/help.navitem.jsx');
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js');

View File

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

View File

@@ -0,0 +1,35 @@
const getLocalStorageMap = function(){
const localStorageMap = {
'AUTOSAVE_ON' : 'HB_editor_autoSaveOn',
'HOMEBREWERY-EDITOR-THEME' : 'HB_editor_theme',
'liveScroll' : 'HB_editor_liveScroll',
'naturalcrit-pane-split' : 'HB_editor_splitWidth',
'HOMEBREWERY-LISTPAGE-SORTDIR' : 'HB_listPage_sortDir',
'HOMEBREWERY-LISTPAGE-SORTTYPE' : 'HB_listPage_sortType',
'HOMEBREWERY-LISTPAGE-VISIBILITY-published' : 'HB_listPage_visibility_group_published',
'HOMEBREWERY-LISTPAGE-VISIBILITY-unpublished' : 'HB_listPage_visibility_group_unpublished',
'hbAdminTab' : 'HB_adminPage_currentTab',
'homebrewery-new' : 'HB_newPage_content',
'homebrewery-new-meta' : 'HB_newPage_metadata',
'homebrewery-new-style' : 'HB_newPage_style',
'homebrewery-recently-edited' : 'HB_nav_recentlyEdited',
'homebrewery-recently-viewed' : 'HB_nav_recentlyViewed',
'hb_toolbarState' : 'HB_renderer_toolbarState',
'hb_toolbarVisibility' : 'HB_renderer_toolbarVisibility'
};
if(global?.account?.username){
const username = global.account.username;
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
}
return localStorageMap;
};
export default getLocalStorageMap;

View File

@@ -0,0 +1,22 @@
import getLocalStorageMap from './localStorageKeyMap.js';
const updateLocalStorage = function(){
// Return if no window and thus no local storage
if(typeof window === 'undefined') return;
const localStorageKeyMap = getLocalStorageMap();
const storage = window.localStorage;
Object.keys(localStorageKeyMap).forEach((key)=>{
if(storage[key]){
if(!storage[localStorageKeyMap[key]]){
const data = storage.getItem(key);
storage.setItem(localStorageKeyMap[key], data);
};
storage.removeItem(key);
}
});
};
export { updateLocalStorage };