mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 16:23:01 +00:00
Compare commits
272 Commits
v3.19.0
...
aligned-us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ede32ccac | ||
|
|
d728126bcc | ||
|
|
02a7920b2c | ||
|
|
43c639246b | ||
|
|
c2e6150edf | ||
|
|
95a1d74644 | ||
|
|
1044aa74b0 | ||
|
|
8a0f350c47 | ||
|
|
6f2c397574 | ||
|
|
8706f91b58 | ||
|
|
90f6e7ec37 | ||
|
|
90a81237ec | ||
|
|
883f59ff0d | ||
|
|
a75364c7f6 | ||
|
|
597ce7cb48 | ||
|
|
d94afa9c50 | ||
|
|
13de195a66 | ||
|
|
32f9a44acf | ||
|
|
bb32f9fe95 | ||
|
|
63f4f5712e | ||
|
|
ede7ad683a | ||
|
|
172c11646a | ||
|
|
bbeac49552 | ||
|
|
1aeded648e | ||
|
|
c1ebc68cd4 | ||
|
|
93b86632fc | ||
|
|
d01860d4de | ||
|
|
86ac11e512 | ||
|
|
9c336062c6 | ||
|
|
2cd47c46f6 | ||
|
|
8671404bdc | ||
|
|
601fc732b0 | ||
|
|
fb3ab47ab0 | ||
|
|
518a3434be | ||
|
|
d01f4fb77e | ||
|
|
6600d9344c | ||
|
|
0371635e11 | ||
|
|
53f6e48f8f | ||
|
|
da578c53a8 | ||
|
|
986bfdd00a | ||
|
|
15c04ef37e | ||
|
|
8cf55932a9 | ||
|
|
759dcb5833 | ||
|
|
83c3eacf83 | ||
|
|
8a788a6ebf | ||
|
|
7198c21229 | ||
|
|
f1ad1b9124 | ||
|
|
593a98db9a | ||
|
|
e25c3daad6 | ||
|
|
96b175e74d | ||
|
|
8924685c26 | ||
|
|
74c9d7b3f1 | ||
|
|
cd378cad0c | ||
|
|
ce304996f0 | ||
|
|
029c105ff1 | ||
|
|
1f81cc9af0 | ||
|
|
6ac6eae863 | ||
|
|
a47a1a25a4 | ||
|
|
0500ac305a | ||
|
|
e1a441b04a | ||
|
|
b98c297079 | ||
|
|
90dfc75ce9 | ||
|
|
dd46a059c5 | ||
|
|
2d881b8dc9 | ||
|
|
e023bfeef6 | ||
|
|
8b351925c1 | ||
|
|
5ddd631dfd | ||
|
|
5ff6327c72 | ||
|
|
c993a1a8c9 | ||
|
|
b9372f17d9 | ||
|
|
6b7c57f0e4 | ||
|
|
6c5063a30d | ||
|
|
e20da7c67f | ||
|
|
3596eabbf5 | ||
|
|
fb4ca21cb4 | ||
|
|
99769c90f8 | ||
|
|
301c50cca9 | ||
|
|
320f1e120f | ||
|
|
cca9ebefdb | ||
|
|
aebc49c2d4 | ||
|
|
1eb226ea13 | ||
|
|
8049b5be9d | ||
|
|
a8dab28fcf | ||
|
|
253dbb358b | ||
|
|
719edd82c5 | ||
|
|
16d7b11b8d | ||
|
|
e2ed7b8600 | ||
|
|
63d957fdc6 | ||
|
|
7751c0e37b | ||
|
|
990bf80b59 | ||
|
|
f16598f238 | ||
|
|
579e9e0ec5 | ||
|
|
f6629f2f9e | ||
|
|
b87c78474d | ||
|
|
958d282a58 | ||
|
|
7e56ae2019 | ||
|
|
ebca50ed4b | ||
|
|
bfd14757c2 | ||
|
|
3626ed5a31 | ||
|
|
d385bacdd6 | ||
|
|
cbbb2c0a7d | ||
|
|
fbe637ff82 | ||
|
|
82bd16c623 | ||
|
|
d1f13af67b | ||
|
|
b6c03e88b8 | ||
|
|
b587d17397 | ||
|
|
0a02f910f8 | ||
|
|
ddfa06e76b | ||
|
|
0c2b1fec04 | ||
|
|
6de7a64acd | ||
|
|
b9fe4c3901 | ||
|
|
5ae01862e5 | ||
|
|
398df7a061 | ||
|
|
443b0f6a37 | ||
|
|
544175b994 | ||
|
|
955602e7ee | ||
|
|
90e577dd3f | ||
|
|
828208aadb | ||
|
|
973e071e93 | ||
|
|
f9e7aa355d | ||
|
|
24dfd41714 | ||
|
|
638e54535d | ||
|
|
cbc6956221 | ||
|
|
248d2038ec | ||
|
|
5b66175b8c | ||
|
|
552aa7d41a | ||
|
|
b0a108b543 | ||
|
|
505d2840c0 | ||
|
|
41ff50fefe | ||
|
|
2fbcc84a50 | ||
|
|
45e4d27c0a | ||
|
|
77bf3ffc6f | ||
|
|
bc045ec6c9 | ||
|
|
6390ea076a | ||
|
|
6affcb587d | ||
|
|
7787afabff | ||
|
|
fb4a8e5cf1 | ||
|
|
8432a6e367 | ||
|
|
90ee08de42 | ||
|
|
40839b18e4 | ||
|
|
677c02cfa5 | ||
|
|
a7a8803e9d | ||
|
|
5fbc111db7 | ||
|
|
5edea7d0f4 | ||
|
|
d3a9d813c9 | ||
|
|
fc475b2a7e | ||
|
|
76b76b3bb6 | ||
|
|
22ef3cbebc | ||
|
|
9da8a17053 | ||
|
|
7cadbfbd7b | ||
|
|
98b9e86787 | ||
|
|
489b4b2694 | ||
|
|
8d279260c2 | ||
|
|
7c08c430d0 | ||
|
|
45689d119e | ||
|
|
c5805af935 | ||
|
|
b2c4bb7082 | ||
|
|
68460447dc | ||
|
|
440c7beff6 | ||
|
|
c7610cf0f8 | ||
|
|
7f3a818558 | ||
|
|
bc82afa5b2 | ||
|
|
abef250631 | ||
|
|
1794e96d50 | ||
|
|
25f25da499 | ||
|
|
aa15bdaacb | ||
|
|
7ba7991631 | ||
|
|
0e1ac26999 | ||
|
|
f49fed8c35 | ||
|
|
a8236fbab4 | ||
|
|
daf4eceedd | ||
|
|
a02361ee65 | ||
|
|
81e20f032e | ||
|
|
1d92b98568 | ||
|
|
0f4157d084 | ||
|
|
4dcc3749d8 | ||
|
|
8f058d56f2 | ||
|
|
d192a064d6 | ||
|
|
cccb531e17 | ||
|
|
6414e73e7d | ||
|
|
41daf8d172 | ||
|
|
4c897fdeb5 | ||
|
|
89ce4de354 | ||
|
|
43095507ee | ||
|
|
eb7fbbe018 | ||
|
|
869958ec38 | ||
|
|
99b90e0998 | ||
|
|
57a48100d3 | ||
|
|
8538e4fadb | ||
|
|
9a002511a3 | ||
|
|
3fa3a52e05 | ||
|
|
4fe920dac3 | ||
|
|
71dff5fbf9 | ||
|
|
26419d2ccb | ||
|
|
f02fe2d8f3 | ||
|
|
318fb53eb2 | ||
|
|
6a32b7427b | ||
|
|
5886bd65e5 | ||
|
|
9c5f80cbdb | ||
|
|
79d8956c4f | ||
|
|
2e491b3556 | ||
|
|
d9a8afa272 | ||
|
|
209195202c | ||
|
|
64235c844a | ||
|
|
5d000a4599 | ||
|
|
380e593b42 | ||
|
|
169f089d08 | ||
|
|
b3977ed141 | ||
|
|
9800561de7 | ||
|
|
166af08e6a | ||
|
|
48f17f7c5e | ||
|
|
87c9f52222 | ||
|
|
c80b7ffd66 | ||
|
|
5f16ce3dbd | ||
|
|
b5ff26f857 | ||
|
|
578b01bbb1 | ||
|
|
67467e0099 | ||
|
|
da21bf20f9 | ||
|
|
df7fcf1e5f | ||
|
|
702ece6671 | ||
|
|
1008321957 | ||
|
|
b547486c48 | ||
|
|
6bb0b8001b | ||
|
|
e1e661976d | ||
|
|
7bdeeee9ef | ||
|
|
becf35d336 | ||
|
|
d7585767c9 | ||
|
|
f9bb6209b7 | ||
|
|
13702a2f62 | ||
|
|
a6a684c89e | ||
|
|
862fa7de89 | ||
|
|
b671cf7b02 | ||
|
|
d5dbe0b4ba | ||
|
|
c2cf695c17 | ||
|
|
6d0d6f08b5 | ||
|
|
77dcc9b433 | ||
|
|
50d2a0d3a2 | ||
|
|
17f60ee159 | ||
|
|
5f2f3a6f3d | ||
|
|
bbb812cb06 | ||
|
|
e842599b22 | ||
|
|
5648e55774 | ||
|
|
c051580545 | ||
|
|
6e72fe2600 | ||
|
|
03602ae1e0 | ||
|
|
8de738a146 | ||
|
|
6960beb739 | ||
|
|
6748639ec5 | ||
|
|
e5651807fd | ||
|
|
9adf6dee61 | ||
|
|
03527a1f95 | ||
|
|
651863b0f7 | ||
|
|
450ecd24b7 | ||
|
|
995cfa2aa4 | ||
|
|
5eecb5ea20 | ||
|
|
0885473b66 | ||
|
|
eabff4f6b2 | ||
|
|
a773df25d0 | ||
|
|
b07f75ac36 | ||
|
|
ed5fbadd73 | ||
|
|
7abf45e8ba | ||
|
|
bbae62e0b7 | ||
|
|
a9d71078d3 | ||
|
|
0ca53f8db6 | ||
|
|
6f52b8473f | ||
|
|
9ecd53267f | ||
|
|
a917937f12 | ||
|
|
b54448f830 | ||
|
|
b88480c9ba | ||
|
|
a8897b2813 | ||
|
|
cb139ae775 | ||
|
|
89a788ff9f |
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -5,6 +5,15 @@ updates:
|
||||
schedule:
|
||||
interval: daily
|
||||
open-pull-requests-limit: 99
|
||||
groups:
|
||||
dev-dependencies:
|
||||
dependency-type: "development"
|
||||
patterns: ["*"]
|
||||
update-types: ["patch", "minor"]
|
||||
prod-dependencies:
|
||||
dependency-type: "production"
|
||||
patterns: ["*"]
|
||||
update-types: ["patch", "minor"]
|
||||
ignore:
|
||||
- dependency-name: eslint
|
||||
versions:
|
||||
|
||||
26
changelog.md
26
changelog.md
@@ -88,6 +88,30 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Wednesday 7/09/2025 - v3.19.3
|
||||
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
* [x] Restoring original saving behavior; will continue investigating why save was failing for some users in background
|
||||
}}
|
||||
|
||||
|
||||
### Wednesday 7/09/2025 - v3.19.2
|
||||
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
* [x] Hotfix for saving issues - Please refresh your browser and report if problems continue
|
||||
}}
|
||||
|
||||
### Wednesday 7/09/2025 - v3.19.1
|
||||
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
* [x] Send diffs instead of full file on save - should help with timeout/disconnect errors
|
||||
}}
|
||||
|
||||
\column
|
||||
|
||||
### Thursday 05/22/2025 - v3.19.0
|
||||
|
||||
{{taskList
|
||||
@@ -130,7 +154,7 @@ Fixes issue [#4146](https://github.com/naturalcrit/homebrewery/issues/4146)
|
||||
New snippet found at {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBERING :fas_arrow_right: :fas_arrow_down_1_9: VARIABLE AUTO PAGE NUMBER**}}
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Fix search bar covering up snippet bar
|
||||
* [x] Fix search bar covering up snippet bar (3 times)
|
||||
|
||||
Fixes issue [#4098](https://github.com/naturalcrit/homebrewery/issues/4098)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import 'naturalcrit/styles/animations.less';
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@import 'naturalcrit/styles/tooltip.less';
|
||||
@import './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
@import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import HeaderNav from './headerNav/headerNav.jsx';
|
||||
import { safeHTML } from './safeHTML.js';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
|
||||
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
|
||||
const PAGE_HEIGHT = 1056;
|
||||
|
||||
const INITIAL_CONTENT = dedent`
|
||||
@@ -37,8 +39,8 @@ const BrewPage = (props)=>{
|
||||
index : 0,
|
||||
...props
|
||||
};
|
||||
const pageRef = useRef(null);
|
||||
const cleanText = safeHTML(`${props.contents}\n<div class="columnSplit"></div>\n`);
|
||||
const pageRef = useRef(null);
|
||||
const cleanText = safeHTML(props.contents);
|
||||
|
||||
useEffect(()=>{
|
||||
if(!pageRef.current) return;
|
||||
@@ -113,13 +115,14 @@ const BrewRenderer = (props)=>{
|
||||
zoomLevel : 100,
|
||||
spread : 'single',
|
||||
startOnRight : true,
|
||||
pageShadows : true
|
||||
pageShadows : true,
|
||||
rowGap : 5,
|
||||
columnGap : 10,
|
||||
});
|
||||
|
||||
//useEffect to store or gather toolbar state from storage
|
||||
useEffect(()=>{
|
||||
const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
|
||||
console.log('toolbar state:', toolbarState);
|
||||
toolbarState && setDisplayOptions(toolbarState);
|
||||
}, []);
|
||||
|
||||
@@ -129,7 +132,7 @@ const BrewRenderer = (props)=>{
|
||||
const pagesRef = useRef(null);
|
||||
|
||||
if(props.renderer == 'legacy') {
|
||||
rawPages = props.text.split('\\page');
|
||||
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
|
||||
} else {
|
||||
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
|
||||
}
|
||||
@@ -186,6 +189,7 @@ const BrewRenderer = (props)=>{
|
||||
let attributes = {};
|
||||
|
||||
if(props.renderer == 'legacy') {
|
||||
pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
|
||||
const html = MarkdownLegacy.render(pageText);
|
||||
|
||||
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
||||
|
||||
@@ -21,8 +21,9 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
}, [visiblePages]);
|
||||
|
||||
useEffect(()=>{
|
||||
const visibility = localStorage.getItem('hb_toolbarVisibility') === 'true';
|
||||
setToolsVisible(visibility);
|
||||
const Visibility = localStorage.getItem('hb_toolbarVisibility');
|
||||
if (Visibility) setToolsVisible(Visibility === 'true');
|
||||
|
||||
}, []);
|
||||
|
||||
const handleZoomButton = (zoom)=>{
|
||||
@@ -68,7 +69,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
} else if(mode == 'fit'){
|
||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||
let minDimRatio;
|
||||
if(displayOptions.spread === 'active')
|
||||
if(displayOptions.spread === 'single')
|
||||
minDimRatio = [...pages].reduce(
|
||||
(minRatio, page)=>Math.min(minRatio,
|
||||
iframeWidth / page.offsetWidth,
|
||||
@@ -165,7 +166,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
|
||||
id='single-spread'
|
||||
className='tool'
|
||||
title='Single Page'
|
||||
onClick={()=>{handleOptionChange('spread', 'active');}}
|
||||
onClick={()=>{handleOptionChange('spread', 'single');}}
|
||||
aria-checked={displayOptions.spread === 'single'}
|
||||
><i className='fac single-spread' /></button>
|
||||
<button role='radio'
|
||||
|
||||
@@ -175,6 +175,10 @@
|
||||
opacity : 0;
|
||||
transition : all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggleButton button i {
|
||||
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +188,4 @@
|
||||
z-index : 5;
|
||||
display : flex;
|
||||
height : 100%;
|
||||
|
||||
button i {
|
||||
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
||||
|
||||
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
/* Any CSS here will apply to your document! */
|
||||
@@ -60,8 +59,9 @@ const Editor = createClass({
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text' //'text', 'style', 'meta', 'snippet'
|
||||
editorTheme : this.props.editorTheme,
|
||||
view : 'text', //'text', 'style', 'meta', 'snippet'
|
||||
snippetbarHeight : 25
|
||||
};
|
||||
},
|
||||
|
||||
@@ -88,6 +88,7 @@ const Editor = createClass({
|
||||
editorTheme : editorTheme
|
||||
});
|
||||
}
|
||||
this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||
@@ -412,6 +413,9 @@ 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 });
|
||||
},
|
||||
|
||||
updateEditorTheme : function(newTheme){
|
||||
@@ -436,7 +440,8 @@ const Editor = createClass({
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
if(this.isStyle()){
|
||||
@@ -449,7 +454,8 @@ const Editor = createClass({
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
if(this.isMeta()){
|
||||
@@ -478,7 +484,8 @@ const Editor = createClass({
|
||||
onChange={this.props.onSnipChange}
|
||||
enableFolding={true}
|
||||
editorTheme={this.state.editorTheme}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
rerenderParent={this.rerenderParent}
|
||||
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||
</>;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
height : 100%;
|
||||
container : editor / inline-size;
|
||||
.codeEditor {
|
||||
height : 100%;
|
||||
height : calc(100% - 25px);
|
||||
.CodeMirror { height : 100%; }
|
||||
.pageLine, .snippetLine {
|
||||
background : #33333328;
|
||||
@@ -108,9 +108,4 @@
|
||||
span { padding : 2px 5px; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@container editor (width < 683px) {
|
||||
.editor .codeEditor .CodeMirror { height : calc(100% - 51px);}
|
||||
.homePage .editor .codeEditor .CodeMirror { height : calc(100% - 25px);}
|
||||
}
|
||||
@@ -1,95 +1,75 @@
|
||||
//╔===--------------- Polyfills --------------===╗//
|
||||
import 'core-js/es/string/to-well-formed.js';
|
||||
//╚===--------------- ---------------===╝//
|
||||
/* eslint-disable camelcase */
|
||||
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
|
||||
import './homebrew.less';
|
||||
import React from 'react';
|
||||
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
|
||||
|
||||
require('./homebrew.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const { StaticRouter:Router } = require('react-router');
|
||||
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
||||
import HomePage from './pages/homePage/homePage.jsx';
|
||||
import EditPage from './pages/editPage/editPage.jsx';
|
||||
import UserPage from './pages/userPage/userPage.jsx';
|
||||
import SharePage from './pages/sharePage/sharePage.jsx';
|
||||
import NewPage from './pages/newPage/newPage.jsx';
|
||||
import ErrorPage from './pages/errorPage/errorPage.jsx';
|
||||
import VaultPage from './pages/vaultPage/vaultPage.jsx';
|
||||
import AccountPage from './pages/accountPage/accountPage.jsx';
|
||||
|
||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||
|
||||
const WithRoute = (props)=>{
|
||||
const WithRoute = ({ el: Element, ...rest })=>{
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryParams = {};
|
||||
for (const [key, value] of searchParams?.entries() || []) {
|
||||
queryParams[key] = value;
|
||||
}
|
||||
const Element = props.el;
|
||||
const allProps = {
|
||||
...props,
|
||||
...params,
|
||||
query : queryParams,
|
||||
el : undefined
|
||||
};
|
||||
return <Element {...allProps} />;
|
||||
const queryParams = Object.fromEntries(searchParams?.entries() || []);
|
||||
|
||||
return <Element {...rest} {...params} query={queryParams} />;
|
||||
};
|
||||
|
||||
const Homebrew = createClass({
|
||||
displayName : 'Homebrewery',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
url : '',
|
||||
welcomeText : '',
|
||||
changelog : '',
|
||||
version : '0.0.0',
|
||||
account : null,
|
||||
enable_v3 : false,
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
lang : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
const Homebrew = (props)=>{
|
||||
const {
|
||||
url = '',
|
||||
version = '0.0.0',
|
||||
account = null,
|
||||
enable_v3 = false,
|
||||
enable_themes,
|
||||
config,
|
||||
brew = {
|
||||
title : '',
|
||||
text : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
lang : ''
|
||||
},
|
||||
userThemes,
|
||||
brews
|
||||
} = props;
|
||||
|
||||
getInitialState : function() {
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
global.enable_v3 = this.props.enable_v3;
|
||||
global.enable_themes = this.props.enable_themes;
|
||||
global.config = this.props.config;
|
||||
global.account = account;
|
||||
global.version = version;
|
||||
global.enable_v3 = enable_v3;
|
||||
global.enable_themes = enable_themes;
|
||||
global.config = config;
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
render : function (){
|
||||
return (
|
||||
<Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Router location={url}>
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
|
||||
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = Homebrew;
|
||||
@@ -1,144 +1,138 @@
|
||||
require('./error-navitem.less');
|
||||
const React = require('react');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const createClass = require('create-react-class');
|
||||
|
||||
const ErrorNavItem = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
error : '',
|
||||
parent : null
|
||||
};
|
||||
},
|
||||
render : function() {
|
||||
const clearError = ()=>{
|
||||
const state = {
|
||||
error : null
|
||||
};
|
||||
if(this.props.parent.state.isSaving) {
|
||||
state.isSaving = false;
|
||||
}
|
||||
this.props.parent.setState(state);
|
||||
};
|
||||
const ErrorNavItem = ({error = '', clearError})=>{
|
||||
const response = error.response;
|
||||
const errorCode = error.code
|
||||
const status = response?.status;
|
||||
const HBErrorCode = response?.body?.HBErrorCode;
|
||||
const message = response?.body?.message;
|
||||
|
||||
const error = this.props.error;
|
||||
const response = error.response;
|
||||
const status = response.status;
|
||||
const HBErrorCode = response.body?.HBErrorCode;
|
||||
const message = response.body?.message;
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${error.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
if(status === 409) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(status === 412) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '04') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
You are no longer signed in as an author of
|
||||
this brew! Were you signed out from a different
|
||||
window? Visit our log in page, then try again!
|
||||
<br></br>
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Can't save because your Google Drive seems to be full!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit our log in page to sign out
|
||||
and sign back in with Google,
|
||||
then try saving again!
|
||||
<br></br>
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '09') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like there was a problem retreiving
|
||||
the theme, or a theme that it inherits,
|
||||
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||
{response.body.brewId}</a> still exists!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '10') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like the brew you have selected
|
||||
as a theme is not tagged for use as a
|
||||
theme. Verify that
|
||||
brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||
{response.body.brewId}</a> has the <span className='lowercase'>meta:theme</span> tag!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${error.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} catch (e){}
|
||||
|
||||
if(status === 409) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
});
|
||||
|
||||
if(status === 412) {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '04') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
You are no longer signed in as an author of
|
||||
this brew! Were you signed out from a different
|
||||
window? Visit our log in page, then try again!
|
||||
<br></br>
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Can't save because your Google Drive seems to be full!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(response?.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit our log in page to sign out
|
||||
and sign back in with Google,
|
||||
then try saving again!
|
||||
<br></br>
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '09') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like there was a problem retreiving
|
||||
the theme, or a theme that it inherits,
|
||||
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||
{response.body.brewId}</a> still exists!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(HBErrorCode === '10') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
Looks like the brew you have selected
|
||||
as a theme is not tagged for use as a
|
||||
theme. Verify that
|
||||
brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||
{response.body.brewId}</a> has the <span className='lowercase'>meta:theme</span> tag!
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(errorCode === 'ECONNABORTED') {
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={clearError}>
|
||||
The request to the server was interrupted or timed out.
|
||||
This can happen due to a network issue, or if
|
||||
trying to save a particularly large brew.
|
||||
Please check your internet connection and try again.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
};
|
||||
|
||||
module.exports = ErrorNavItem;
|
||||
|
||||
@@ -5,33 +5,45 @@ const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); //
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const NewBrew = ()=>{
|
||||
const handleFileChange = (e)=>{
|
||||
const file = e.target.files[0];
|
||||
if(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e)=>{
|
||||
const fileContent = e.target.result;
|
||||
const newBrew = {
|
||||
text : fileContent,
|
||||
style : ''
|
||||
};
|
||||
if(fileContent.startsWith('```metadata')) {
|
||||
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
|
||||
localStorage.setItem(BREWKEY, newBrew.text);
|
||||
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
|
||||
window.location.href = '/new';
|
||||
} else {
|
||||
alert('This file is invalid, please, enter a valid file');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
if(!file) return;
|
||||
|
||||
const currentNew = localStorage.getItem(BREWKEY);
|
||||
if(currentNew && !confirm(
|
||||
`You have some text in the new brew space, if you load a file that text will be lost, are you sure you want to load the file?`
|
||||
)) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e)=>{
|
||||
const fileContent = e.target.result;
|
||||
const newBrew = { text: fileContent, style: '' };
|
||||
|
||||
if(fileContent.startsWith('```metadata')) {
|
||||
splitTextStyleAndMetadata(newBrew);
|
||||
localStorage.setItem(BREWKEY, newBrew.text);
|
||||
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify(
|
||||
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])
|
||||
));
|
||||
window.location.href = '/new';
|
||||
return;
|
||||
}
|
||||
|
||||
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.`);
|
||||
|
||||
|
||||
console.log(file);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Nav.dropdown>
|
||||
<Nav.item
|
||||
|
||||
@@ -5,7 +5,7 @@ const moment = require('moment');
|
||||
import request from '../../../../utils/request-middleware.js';
|
||||
|
||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||
const homebreweryIcon = require('../../../../thumbnail.svg');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = ({
|
||||
|
||||
@@ -115,15 +115,15 @@
|
||||
}
|
||||
}
|
||||
.googleDriveIcon {
|
||||
height : 18px;
|
||||
padding : 0px;
|
||||
margin : -5px;
|
||||
height : 18px;
|
||||
}
|
||||
.homebreweryIcon {
|
||||
position : relative;
|
||||
top : 5px;
|
||||
left : -5px;
|
||||
height : 24px;
|
||||
mix-blend-mode : darken;
|
||||
position : relative;
|
||||
padding : 0px;
|
||||
top : 5px;
|
||||
left : -7.5px;
|
||||
height : 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
&::before {
|
||||
margin-right : 5px;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-weight : 900;
|
||||
content : '\f00c';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,431 +1,377 @@
|
||||
/* eslint-disable max-lines */
|
||||
require('./editPage.less');
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const createClass = require('create-react-class');
|
||||
import './editPage.less';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
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';
|
||||
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
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 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';
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
||||
import LockNotification from './lockNotification/lockNotification.jsx';
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js';
|
||||
|
||||
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||
|
||||
const googleDriveIcon = require('../../googleDrive.svg');
|
||||
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 EditPage = createClass({
|
||||
displayName : 'EditPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW_LOAD
|
||||
};
|
||||
},
|
||||
const EditPage = (props)=>{
|
||||
props = {
|
||||
brew : DEFAULT_BREW_LOAD,
|
||||
...props
|
||||
};
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||
alertLoginToTransfer : false,
|
||||
saveGoogle : this.props.brew.googleId ? true : false,
|
||||
confirmGoogleTransfer : false,
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
url : '',
|
||||
autoSave : true,
|
||||
autoSaveWarning : false,
|
||||
unsavedTime : new Date(),
|
||||
currentEditorViewPageNum : 1,
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
displayLockMessage : this.props.brew.lock || false,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
const [currentBrew , setCurrentBrew ] = useState(props.brew);
|
||||
const [isSaving , setIsSaving ] = useState(false);
|
||||
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId);
|
||||
const [error , setError ] = useState(null);
|
||||
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 [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
|
||||
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
|
||||
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
|
||||
const [url , setUrl ] = useState('');
|
||||
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
|
||||
const [autoSaveWarning , setAutoSaveWarning ] = useState(true);
|
||||
const [unsavedTime , setUnsavedTime ] = useState(new Date());
|
||||
|
||||
editor : React.createRef(null),
|
||||
savedBrew : null,
|
||||
const editorRef = useRef(null);
|
||||
const savedBrew = useRef(_.cloneDeep(props.brew));
|
||||
const warningTimer = useRef(null);
|
||||
const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []);
|
||||
|
||||
componentDidMount : function(){
|
||||
this.setState({
|
||||
url : window.location.href
|
||||
});
|
||||
useEffect(()=>{
|
||||
setUrl(window.location.href);
|
||||
|
||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||
|
||||
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
||||
if(this.state.autoSave){
|
||||
this.trySave();
|
||||
} else {
|
||||
this.setState({ autoSaveWarning: true });
|
||||
}
|
||||
});
|
||||
const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true);
|
||||
setAutoSaveEnabled(autoSavePref);
|
||||
setAutoSaveWarning(!autoSavePref);
|
||||
setHTMLErrors(Markdown.validate(currentBrew.text));
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
if(isSaving || unsavedChanges)
|
||||
return 'You have unsaved changes!';
|
||||
}
|
||||
};
|
||||
|
||||
this.setState((prevState)=>({
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, savedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.onbeforeunload = function(){};
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentDidUpdate : function(){
|
||||
const hasChange = this.hasChanges();
|
||||
if(this.state.isPending != hasChange){
|
||||
this.setState({
|
||||
isPending : hasChange
|
||||
});
|
||||
}
|
||||
},
|
||||
if(hasChange && autoSaveEnabled) trySave();
|
||||
}, [currentBrew]);
|
||||
|
||||
handleControlKeys : function(e){
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.trySave(true);
|
||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
if(e.keyCode === S_KEY) trySave(true);
|
||||
if(e.keyCode === P_KEY) printCurrentBrew();
|
||||
if(e.keyCode === S_KEY || e.keyCode === P_KEY) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.editor.current.update();
|
||||
},
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current?.update();
|
||||
};
|
||||
|
||||
handleEditorViewPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||
},
|
||||
const handleEditorViewPageChange = (pageNumber)=>{
|
||||
setCurrentEditorViewPageNum(pageNumber);
|
||||
};
|
||||
|
||||
handleEditorCursorPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||
},
|
||||
const handleEditorCursorPageChange = (pageNumber)=>{
|
||||
setCurrentEditorCursorPageNum(pageNumber);
|
||||
};
|
||||
|
||||
handleBrewRendererPageChange : function(pageNumber){
|
||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||
},
|
||||
const handleBrewRendererPageChange = (pageNumber)=>{
|
||||
setCurrentBrewRendererPageNum(pageNumber);
|
||||
};
|
||||
|
||||
handleTextChange : function(text){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
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 }));
|
||||
};
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
const handleStyleChange = (style)=>{
|
||||
setCurrentBrew((prevBrew)=>({ ...prevBrew, style }));
|
||||
};
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
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 }));
|
||||
};
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
const handleMetaChange = (metadata, field = undefined)=>{
|
||||
if(field === 'theme' || field === 'renderer')
|
||||
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style }
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
setCurrentBrew((prev)=>({ ...prev, ...metadata }));
|
||||
};
|
||||
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
|
||||
...prevBrew,
|
||||
style : newData.style,
|
||||
text : newData.text,
|
||||
snippets : newData.snippets
|
||||
}));
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
...metadata
|
||||
}
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
const trySave = (immediate = false)=>{
|
||||
//debounceSave = _.debounce(save, SAVE_TIMEOUT);
|
||||
if(isSaving) return;
|
||||
|
||||
hasChanges : function(){
|
||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||
},
|
||||
const hasChange = !_.isEqual(currentBrew, savedBrew.current);
|
||||
|
||||
updateBrew : function(newData){
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
style : newData.style,
|
||||
text : newData.text
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
trySave : function(immediate=false){
|
||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
if(this.hasChanges()){
|
||||
this.debounceSave();
|
||||
} else {
|
||||
this.debounceSave.cancel();
|
||||
}
|
||||
if(immediate) this.debounceSave.flush();
|
||||
},
|
||||
|
||||
handleGoogleClick : function(){
|
||||
if(!global.account?.googleId) {
|
||||
this.setState({
|
||||
alertLoginToTransfer : true
|
||||
});
|
||||
if(immediate) {
|
||||
debounceSave(currentBrew, saveGoogle);
|
||||
debounceSave.flush?.();
|
||||
return;
|
||||
}
|
||||
this.setState((prevState)=>({
|
||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||
}));
|
||||
this.setState({
|
||||
error : null,
|
||||
isSaving : false
|
||||
});
|
||||
},
|
||||
|
||||
closeAlerts : function(event){
|
||||
event.stopPropagation(); //Only handle click once so alert doesn't reopen
|
||||
this.setState({
|
||||
alertTrashedGoogleBrew : false,
|
||||
alertLoginToTransfer : false,
|
||||
confirmGoogleTransfer : false
|
||||
});
|
||||
},
|
||||
if(hasChange)
|
||||
debounceSave(currentBrew, saveGoogle);
|
||||
else
|
||||
debounceSave.cancel?.();
|
||||
};
|
||||
|
||||
toggleGoogleStorage : function(){
|
||||
this.setState((prevState)=>({
|
||||
saveGoogle : !prevState.saveGoogle,
|
||||
isSaving : false,
|
||||
error : null
|
||||
}), ()=>this.save());
|
||||
},
|
||||
const handleGoogleClick = ()=>{
|
||||
if(!global.account?.googleId) {
|
||||
setAlertLoginToTransfer(true);
|
||||
return;
|
||||
}
|
||||
|
||||
save : async function(){
|
||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||
setConfirmGoogleTransfer((prev)=>!prev);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
this.setState((prevState)=>({
|
||||
isSaving : true,
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
const closeAlerts = (e)=>{
|
||||
e.stopPropagation(); //Only handle click once so alert doesn't reopen
|
||||
setAlertTrashedGoogleBrew(false);
|
||||
setAlertLoginToTransfer(false);
|
||||
setConfirmGoogleTransfer(false);
|
||||
};
|
||||
|
||||
await updateHistory(this.state.brew).catch(console.error);
|
||||
const toggleGoogleStorage = ()=>{
|
||||
setSaveGoogle((prev)=>!prev);
|
||||
setError(null);
|
||||
trySave(true);
|
||||
};
|
||||
|
||||
const save = async (brew, saveToGoogle)=>{
|
||||
debounceSave?.cancel?.();
|
||||
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
setHTMLErrors(Markdown.validate(brew.text));
|
||||
|
||||
await updateHistory(brew).catch(console.error);
|
||||
await versionHistoryGarbageCollection().catch(console.error);
|
||||
|
||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||
//Prepare content to send to server
|
||||
const brewToSave = {
|
||||
...brew,
|
||||
text : brew.text.normalize('NFC'),
|
||||
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
|
||||
patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
|
||||
hash : await md5(savedBrew.current.text),
|
||||
textBin : undefined
|
||||
};
|
||||
|
||||
const brew = this.state.brew;
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave)));
|
||||
const transfer = saveToGoogle === _.isNil(brew.googleId);
|
||||
const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : '';
|
||||
|
||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}${params}`)
|
||||
.send(brew)
|
||||
.put(`/api/update/${brewToSave.editId}${params}`)
|
||||
.set('Content-Encoding', 'gzip')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(compressedBrew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ error: err });
|
||||
console.error('Error Updating Local Brew');
|
||||
setError(err);
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
this.savedBrew = {
|
||||
...this.state.brew,
|
||||
googleId : res.body.googleId ? res.body.googleId : null,
|
||||
editId : res.body.editId,
|
||||
shareId : res.body.shareId,
|
||||
version : res.body.version
|
||||
const { googleId, editId, shareId, version } = res.body;
|
||||
|
||||
savedBrew.current = {
|
||||
...brew,
|
||||
googleId : googleId ?? null,
|
||||
editId,
|
||||
shareId,
|
||||
version
|
||||
};
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
|
||||
this.setState(()=>({
|
||||
brew : this.savedBrew,
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
unsavedTime : new Date()
|
||||
}));
|
||||
},
|
||||
setCurrentBrew(savedBrew.current);
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
|
||||
setIsSaving(false);
|
||||
setUnsavedTime(new Date());
|
||||
|
||||
{this.state.confirmGoogleTransfer &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
{ this.state.saveGoogle
|
||||
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
|
||||
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
|
||||
}
|
||||
history.replaceState(null, null, `/edit/${editId}`);
|
||||
};
|
||||
|
||||
const renderGoogleDriveIcon = ()=>(
|
||||
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}>
|
||||
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' />
|
||||
|
||||
{confirmGoogleTransfer && (
|
||||
<div className='errorContainer' onClick={closeAlerts}>
|
||||
{saveGoogle
|
||||
? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?'
|
||||
: 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
|
||||
<br />
|
||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
||||
Yes
|
||||
</div>
|
||||
<div className='deny'>
|
||||
No
|
||||
</div>
|
||||
<div className='confirm' onClick={toggleGoogleStorage}> Yes </div>
|
||||
<div className='deny'> No </div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{this.state.alertLoginToTransfer &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
You must be signed in to a Google account to transfer
|
||||
between the homebrewery and Google Drive!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
{alertLoginToTransfer && (
|
||||
<div className='errorContainer' onClick={closeAlerts}>
|
||||
You must be signed in to a Google account to transfer between the homebrewery and Google Drive!
|
||||
<a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${url}`}>
|
||||
<div className='confirm'> Sign In </div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
<div className='deny'> Not Now </div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{this.state.alertTrashedGoogleBrew &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'>
|
||||
OK
|
||||
</div>
|
||||
{alertTrashedGoogleBrew && (
|
||||
<div className='errorContainer' onClick={closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />
|
||||
If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'> OK </div>
|
||||
</div>
|
||||
}
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
)}
|
||||
</Nav.item>
|
||||
);
|
||||
|
||||
const renderSaveButton = ()=>{
|
||||
// #1 - Currently saving, show SAVING
|
||||
if(this.state.isSaving){
|
||||
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(this.state.isPending && this.state.autoSaveWarning){
|
||||
this.setAutosaveWarning();
|
||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
if(unsavedChanges && autoSaveWarning) {
|
||||
resetAutoSaveWarning();
|
||||
const elapsedTime = Math.round((new Date() - unsavedTime) / 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>;
|
||||
Reminder...
|
||||
<div className='errorContainer'>{text}</div>
|
||||
</Nav.item>
|
||||
}
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
// Use trySave(true) instead of save() to use debounced save function
|
||||
if(this.state.isPending){
|
||||
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
if(unsavedChanges)
|
||||
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(this.state.autoSave){
|
||||
if(autoSaveEnabled)
|
||||
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
||||
}
|
||||
|
||||
// DEFAULT - No unsaved changes, show SAVED
|
||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||
},
|
||||
};
|
||||
|
||||
handleAutoSave : function(){
|
||||
if(this.warningTimer) clearTimeout(this.warningTimer);
|
||||
this.setState((prevState)=>({
|
||||
autoSave : !prevState.autoSave,
|
||||
autoSaveWarning : prevState.autoSave
|
||||
}), ()=>{
|
||||
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
|
||||
});
|
||||
},
|
||||
const toggleAutoSave = ()=>{
|
||||
if(warningTimer.current) clearTimeout(warningTimer.current);
|
||||
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled));
|
||||
setAutoSaveWarning(autoSaveWarning);
|
||||
setAutoSaveEnabled(!autoSaveEnabled);
|
||||
};
|
||||
|
||||
setAutosaveWarning : function(){
|
||||
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
|
||||
this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
|
||||
this.warningTimer;
|
||||
},
|
||||
const resetAutoSaveWarning = ()=>{
|
||||
setTimeout(()=>setAutoSaveWarning(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
|
||||
warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved changes warnings
|
||||
};
|
||||
|
||||
errorReported : function(error) {
|
||||
this.setState({
|
||||
error
|
||||
});
|
||||
},
|
||||
const renderAutoSaveButton = ()=>(
|
||||
<Nav.item onClick={toggleAutoSave}>
|
||||
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||
</Nav.item>
|
||||
);
|
||||
|
||||
renderAutoSaveButton : function(){
|
||||
return <Nav.item onClick={this.handleAutoSave}>
|
||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||
</Nav.item>;
|
||||
},
|
||||
const processShareId = ()=>(
|
||||
currentBrew.googleId && !currentBrew.stubbed
|
||||
? currentBrew.googleId + currentBrew.shareId
|
||||
: currentBrew.shareId
|
||||
);
|
||||
|
||||
processShareId : function() {
|
||||
return this.state.brew.googleId && !this.state.brew.stubbed ?
|
||||
this.state.brew.googleId + this.state.brew.shareId :
|
||||
this.state.brew.shareId;
|
||||
},
|
||||
|
||||
getRedditLink : function(){
|
||||
|
||||
const shareLink = this.processShareId();
|
||||
const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
|
||||
const title = `${this.props.brew.title} ${systems}`;
|
||||
const getRedditLink = ()=>{
|
||||
const shareLink = processShareId();
|
||||
const systems = currentBrew.systems.length > 0 ? ` [${currentBrew.systems.join(' - ')}]` : '';
|
||||
const title = `${currentBrew.title} ${systems}`;
|
||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||
|
||||
**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
|
||||
**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
|
||||
|
||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||
},
|
||||
};
|
||||
|
||||
renderNavbar : function(){
|
||||
const shareLink = this.processShareId();
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>{
|
||||
const shareLink = processShareId();
|
||||
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
<Nav.dropdown className='save-menu'>
|
||||
{this.renderSaveButton()}
|
||||
{this.renderAutoSaveButton()}
|
||||
</Nav.dropdown>
|
||||
}
|
||||
<NewBrew />
|
||||
{renderGoogleDriveIcon()}
|
||||
{error
|
||||
? <ErrorNavItem error={error} clearError={clearError} />
|
||||
: <Nav.dropdown className='save-menu'>
|
||||
{renderSaveButton()}
|
||||
{renderAutoSaveButton()}
|
||||
</Nav.dropdown>}
|
||||
<NewBrewItem/>
|
||||
<HelpNavItem/>
|
||||
<Nav.dropdown>
|
||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||
@@ -437,63 +383,64 @@ const EditPage = createClass({
|
||||
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
|
||||
copy url
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||
<Nav.item color='blue' href={getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||
post to reddit
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
<PrintNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||
<Account />
|
||||
<RecentNavItem brew={currentBrew} storageKey='edit' />
|
||||
<AccountNavItem/>
|
||||
</Nav.section>
|
||||
|
||||
</Navbar>;
|
||||
},
|
||||
};
|
||||
|
||||
render : function(){
|
||||
return <div className='editPage sitePage'>
|
||||
return (
|
||||
<div className='editPage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
|
||||
{renderNavbar()}
|
||||
|
||||
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<SplitPane onDragFinish={handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
reportError={this.errorReported}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
updateBrew={this.updateBrew}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
ref={editorRef}
|
||||
brew={currentBrew}
|
||||
onTextChange={handleTextChange}
|
||||
onStyleChange={handleStyleChange}
|
||||
onSnipChange={handleSnipChange}
|
||||
onMetaChange={handleMetaChange}
|
||||
reportError={setError}
|
||||
renderer={currentBrew.renderer}
|
||||
userThemes={props.userThemes}
|
||||
themeBundle={themeBundle}
|
||||
updateBrew={updateBrew}
|
||||
onCursorPageChange={handleEditorCursorPageChange}
|
||||
onViewPageChange={handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
theme={currentBrew.theme}
|
||||
themeBundle={themeBundle}
|
||||
errors={HTMLErrors}
|
||||
lang={currentBrew.lang}
|
||||
onPageChange={handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = EditPage;
|
||||
|
||||
@@ -176,6 +176,26 @@ const errorIndex = (props)=>{
|
||||
|
||||
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
||||
|
||||
// ID validation error
|
||||
'11' : dedent`
|
||||
## No Homebrewery document could be found.
|
||||
|
||||
The server could not locate the Homebrewery document. The Brew ID failed the validation check.
|
||||
|
||||
:
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Google ID validation error
|
||||
'12' : dedent`
|
||||
## No Google document could be found.
|
||||
|
||||
The server could not locate the Google document. The Google ID failed the validation check.
|
||||
|
||||
:
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
//account page when account is not defined
|
||||
'50' : dedent`
|
||||
## You are not signed in
|
||||
|
||||
@@ -1,90 +1,91 @@
|
||||
require('./homePage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
import request from '../../utils/request-middleware.js';
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
import './homePage.less';
|
||||
|
||||
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 VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
import React from 'react';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { Meta } from 'vitreum/headtags';
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
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';
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
const HomePage = createClass({
|
||||
displayName : 'HomePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0'
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
welcomeText : this.props.brew.text,
|
||||
error : undefined,
|
||||
currentEditorViewPageNum : 1,
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
|
||||
editor : React.createRef(null),
|
||||
const HomePage =(props)=>{
|
||||
props = {
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0',
|
||||
...props
|
||||
};
|
||||
|
||||
componentDidMount : function() {
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
},
|
||||
const [brew , setBrew] = useState(props.brew);
|
||||
const [welcomeText , setWelcomeText] = useState(props.brew.text);
|
||||
const [error , setError] = useState(undefined);
|
||||
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
|
||||
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||
const [themeBundle , setThemeBundle] = useState({});
|
||||
const [isSaving , setIsSaving] = useState(false);
|
||||
|
||||
handleSave : function(){
|
||||
const editorRef = useRef(null);
|
||||
|
||||
useEffect(()=>{
|
||||
fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme);
|
||||
}, []);
|
||||
|
||||
const save = ()=>{
|
||||
request.post('/api')
|
||||
.send(this.state.brew)
|
||||
.send(brew)
|
||||
.end((err, res)=>{
|
||||
if(err) {
|
||||
this.setState({ error: err });
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
const brew = res.body;
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
const saved = res.body;
|
||||
window.location = `/edit/${saved.editId}`;
|
||||
});
|
||||
},
|
||||
handleSplitMove : function(){
|
||||
this.editor.current.update();
|
||||
},
|
||||
};
|
||||
|
||||
handleEditorViewPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||
},
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current.update();
|
||||
};
|
||||
|
||||
handleEditorCursorPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||
},
|
||||
const handleEditorViewPageChange = (pageNumber)=>{
|
||||
setCurrentEditorViewPageNum(pageNumber);
|
||||
};
|
||||
|
||||
const handleEditorCursorPageChange = (pageNumber)=>{
|
||||
setCurrentEditorCursorPageNum(pageNumber);
|
||||
};
|
||||
|
||||
const handleBrewRendererPageChange = (pageNumber)=>{
|
||||
setCurrentBrewRendererPageNum(pageNumber);
|
||||
};
|
||||
|
||||
handleBrewRendererPageChange : function(pageNumber){
|
||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||
},
|
||||
const handleTextChange = (text)=>{
|
||||
setBrew((prevBrew) => ({ ...prevBrew, text }));
|
||||
};
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
}));
|
||||
},
|
||||
renderNavbar : function(){
|
||||
return <Navbar ver={this.props.ver}>
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>{
|
||||
return <Navbar ver={props.ver}>
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
{error ?
|
||||
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
|
||||
null
|
||||
}
|
||||
<NewBrewItem />
|
||||
@@ -94,48 +95,48 @@ const HomePage = createClass({
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
};
|
||||
|
||||
render : function(){
|
||||
return <div className='homePage sitePage'>
|
||||
return (
|
||||
<div className='homePage sitePage'>
|
||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||
{this.renderNavbar()}
|
||||
{renderNavbar()}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<SplitPane onDragFinish={handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
ref={editorRef}
|
||||
brew={brew}
|
||||
onTextChange={handleTextChange}
|
||||
renderer={brew.renderer}
|
||||
showEditButtons={false}
|
||||
themeBundle={this.state.themeBundle}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
themeBundle={themeBundle}
|
||||
onCursorPageChange={handleEditorCursorPageChange}
|
||||
onViewPageChange={handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
themeBundle={this.state.themeBundle}
|
||||
text={brew.text}
|
||||
style={brew.style}
|
||||
renderer={brew.renderer}
|
||||
onPageChange={handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
themeBundle={themeBundle}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
||||
<div className={`floatingSaveButton${welcomeText !== brew.text ? ' show' : ''}`} onClick={save}>
|
||||
Save current <i className='fas fa-save' />
|
||||
</div>
|
||||
|
||||
<a href='/new' className='floatingNewButton'>
|
||||
Create your own <i className='fas fa-magic' />
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
module.exports = HomePage;
|
||||
|
||||
@@ -1,276 +1,251 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./newPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import './newPage.less';
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
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';
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
let SAVEKEY;
|
||||
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||
|
||||
const NewPage = (props) => {
|
||||
props = {
|
||||
brew: DEFAULT_BREW,
|
||||
...props
|
||||
};
|
||||
|
||||
const NewPage = createClass({
|
||||
displayName : 'NewPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW
|
||||
const [currentBrew , setCurrentBrew ] = useState(props.brew);
|
||||
const [isSaving , setIsSaving ] = useState(false);
|
||||
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
|
||||
const [error , setError ] = useState(null);
|
||||
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 editorRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
loadBrew();
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
};
|
||||
},
|
||||
}, []);
|
||||
|
||||
getInitialState : function() {
|
||||
const brew = this.props.brew;
|
||||
|
||||
return {
|
||||
brew : brew,
|
||||
isSaving : false,
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(brew.text),
|
||||
currentEditorViewPageNum : 1,
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
|
||||
editor : React.createRef(null),
|
||||
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
const brew = this.state.brew;
|
||||
|
||||
if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||
const loadBrew = ()=>{
|
||||
const brew = { ...currentBrew };
|
||||
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||
const brewStorage = localStorage.getItem(BREWKEY);
|
||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
|
||||
brew.text = brewStorage ?? brew.text;
|
||||
brew.style = styleStorage ?? brew.style;
|
||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||
brew.text = brewStorage ?? brew.text;
|
||||
brew.style = styleStorage ?? brew.style;
|
||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||
}
|
||||
|
||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||
|
||||
this.setState({
|
||||
brew : brew,
|
||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||
});
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
setCurrentBrew(brew);
|
||||
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
if(brew.style)
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||
if(window.location.pathname != '/new') {
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang }));
|
||||
if(window.location.pathname !== '/new')
|
||||
window.history.replaceState({}, window.location.title, '/new/');
|
||||
}
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
};
|
||||
|
||||
handleControlKeys : function(e){
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const handleControlKeys = (e) => {
|
||||
if (!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.save();
|
||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
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();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.editor.current.update();
|
||||
},
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current.update();
|
||||
};
|
||||
|
||||
handleEditorViewPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||
},
|
||||
const handleEditorViewPageChange = (pageNumber)=>{
|
||||
setCurrentEditorViewPageNum(pageNumber);
|
||||
};
|
||||
|
||||
const handleEditorCursorPageChange = (pageNumber)=>{
|
||||
setCurrentEditorCursorPageNum(pageNumber);
|
||||
};
|
||||
|
||||
const handleBrewRendererPageChange = (pageNumber)=>{
|
||||
setCurrentBrewRendererPageNum(pageNumber);
|
||||
};
|
||||
|
||||
handleEditorCursorPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||
},
|
||||
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);
|
||||
|
||||
handleBrewRendererPageChange : function(pageNumber){
|
||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
htmlErrors : htmlErrors,
|
||||
}));
|
||||
setHTMLErrors(HTMLErrors);
|
||||
setCurrentBrew((prevBrew) => ({ ...prevBrew, text }));
|
||||
localStorage.setItem(BREWKEY, text);
|
||||
},
|
||||
};
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style },
|
||||
}));
|
||||
const handleStyleChange = (style) => {
|
||||
setCurrentBrew(prevBrew => ({ ...prevBrew, style }));
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
};
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
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);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
setHTMLErrors(HTMLErrors);
|
||||
setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet }));
|
||||
};
|
||||
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
const handleMetaChange = (metadata, field = undefined) => {
|
||||
if (field === 'theme' || field === 'renderer')
|
||||
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, ...metadata },
|
||||
}), ()=>{
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
// 'title' : this.state.brew.title,
|
||||
// 'description' : this.state.brew.description,
|
||||
'renderer' : this.state.brew.renderer,
|
||||
'theme' : this.state.brew.theme,
|
||||
'lang' : this.state.brew.lang
|
||||
}));
|
||||
});
|
||||
;
|
||||
},
|
||||
setCurrentBrew(prev => ({ ...prev, ...metadata }));
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : metadata.renderer,
|
||||
theme : metadata.theme,
|
||||
lang : metadata.lang
|
||||
}));
|
||||
};
|
||||
|
||||
save : async function(){
|
||||
this.setState({
|
||||
isSaving : true
|
||||
});
|
||||
const save = async () => {
|
||||
setIsSaving(true);
|
||||
|
||||
let brew = this.state.brew;
|
||||
// Split out CSS to Style if CSS codefence exists
|
||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
let updatedBrew = { ...currentBrew };
|
||||
splitTextStyleAndMetadata(updatedBrew);
|
||||
|
||||
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
|
||||
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
||||
|
||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
const res = await request
|
||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
this.setState({ isSaving: false, error: err });
|
||||
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(updatedBrew)
|
||||
.catch((err) => {
|
||||
setIsSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
brew = res.body;
|
||||
setIsSaving(false)
|
||||
if (!res) return;
|
||||
|
||||
const savedBrew = res.body;
|
||||
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
},
|
||||
window.location = `/edit/${savedBrew.editId}`;
|
||||
};
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.isSaving){
|
||||
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={this.save}>
|
||||
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
|
||||
save
|
||||
</Nav.item>;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = () => (
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
this.renderSaveButton()
|
||||
}
|
||||
{error
|
||||
? <ErrorNavItem error={error} clearError={clearError} />
|
||||
: renderSaveButton()}
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
render : function(){
|
||||
return <div className='newPage sitePage'>
|
||||
{this.renderNavbar()}
|
||||
return (
|
||||
<div className='newPage sitePage'>
|
||||
{renderNavbar()}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<SplitPane onDragFinish={handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
ref={editorRef}
|
||||
brew={currentBrew}
|
||||
onTextChange={handleTextChange}
|
||||
onStyleChange={handleStyleChange}
|
||||
onMetaChange={handleMetaChange}
|
||||
onSnipChange={handleSnipChange}
|
||||
renderer={currentBrew.renderer}
|
||||
userThemes={props.userThemes}
|
||||
themeBundle={themeBundle}
|
||||
onCursorPageChange={handleEditorCursorPageChange}
|
||||
onViewPageChange={handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
theme={currentBrew.theme}
|
||||
themeBundle={themeBundle}
|
||||
errors={HTMLErrors}
|
||||
lang={currentBrew.lang}
|
||||
onPageChange={handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = NewPage;
|
||||
|
||||
@@ -17,15 +17,11 @@ const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpe
|
||||
const SharePage = (props)=>{
|
||||
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
|
||||
|
||||
const [state, setState] = useState({
|
||||
themeBundle : {},
|
||||
currentBrewRendererPageNum : 1,
|
||||
});
|
||||
const [themeBundle, setThemeBundle] = useState({});
|
||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||
|
||||
const handleBrewRendererPageChange = useCallback((pageNumber)=>{
|
||||
setState((prevState)=>({
|
||||
currentBrewRendererPageNum : pageNumber,
|
||||
...prevState }));
|
||||
setCurrentBrewRendererPageNum(pageNumber);
|
||||
}, []);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
@@ -40,11 +36,7 @@ const SharePage = (props)=>{
|
||||
|
||||
useEffect(()=>{
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
fetchThemeBundle(
|
||||
{ setState },
|
||||
brew.renderer,
|
||||
brew.theme
|
||||
);
|
||||
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme);
|
||||
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
@@ -114,9 +106,9 @@ const SharePage = (props)=>{
|
||||
lang={brew.lang}
|
||||
renderer={brew.renderer}
|
||||
theme={brew.theme}
|
||||
themeBundle={state.themeBundle}
|
||||
themeBundle={themeBundle}
|
||||
onPageChange={handleBrewRendererPageChange}
|
||||
currentBrewRendererPageNum={state.currentBrewRendererPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -39,10 +39,14 @@ const UserPage = (props)=>{
|
||||
}] : [])
|
||||
];
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const navItems = (
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
{error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
|
||||
{error && (<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>)}
|
||||
<NewBrew />
|
||||
<HelpNavItem />
|
||||
<VaultNavitem />
|
||||
|
||||
@@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
||||
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
||||
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
|
||||
64
client/homebrew/thumbnail.svg
Normal file
64
client/homebrew/thumbnail.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 94.65 94.6"
|
||||
version="1.1"
|
||||
id="svg11"
|
||||
sodipodi:docname="thumbnail.svg"
|
||||
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<sodipodi:namedview
|
||||
id="namedview13"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:showpageshadow="0"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="8.4989431"
|
||||
inkscape:cx="38.887188"
|
||||
inkscape:cy="47.417661"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1043"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg11" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title6">NaturalCritLogo</title>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
style="fill:#000000;stroke:#000000">
|
||||
<g
|
||||
id="base"
|
||||
style="fill:#000000;stroke:#000000">
|
||||
<path
|
||||
id="D20"
|
||||
class="cls-1"
|
||||
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||
style="fill:#000000;fill-opacity:1;stroke:#000000" />
|
||||
</g>
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>NaturalCritLogo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
74
client/homebrew/utils/request-middleware.spec.js
Normal file
74
client/homebrew/utils/request-middleware.spec.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import requestMiddleware from './request-middleware';
|
||||
|
||||
jest.mock('superagent');
|
||||
import request from 'superagent';
|
||||
|
||||
describe('request-middleware', ()=>{
|
||||
let version;
|
||||
|
||||
let setFn;
|
||||
let testFn;
|
||||
|
||||
beforeEach(()=>{
|
||||
jest.resetAllMocks();
|
||||
version = global.version;
|
||||
|
||||
global.version = '999';
|
||||
|
||||
setFn = jest.fn();
|
||||
testFn = jest.fn(()=>{ return { set: setFn }; });
|
||||
});
|
||||
|
||||
afterEach(()=>{
|
||||
global.version = version;
|
||||
});
|
||||
|
||||
it('should add header to get', ()=>{
|
||||
// Ensure tests functions have been reset
|
||||
expect(testFn).not.toHaveBeenCalled();
|
||||
expect(setFn).not.toHaveBeenCalled();
|
||||
|
||||
request.get = testFn;
|
||||
|
||||
requestMiddleware.get('path');
|
||||
|
||||
expect(testFn).toHaveBeenCalledWith('path');
|
||||
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||
});
|
||||
|
||||
it('should add header to put', ()=>{
|
||||
expect(testFn).not.toHaveBeenCalled();
|
||||
expect(setFn).not.toHaveBeenCalled();
|
||||
|
||||
request.put = testFn;
|
||||
|
||||
requestMiddleware.put('path');
|
||||
|
||||
expect(testFn).toHaveBeenCalledWith('path');
|
||||
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||
});
|
||||
|
||||
it('should add header to post', ()=>{
|
||||
expect(testFn).not.toHaveBeenCalled();
|
||||
expect(setFn).not.toHaveBeenCalled();
|
||||
|
||||
request.post = testFn;
|
||||
|
||||
requestMiddleware.post('path');
|
||||
|
||||
expect(testFn).toHaveBeenCalledWith('path');
|
||||
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||
});
|
||||
|
||||
it('should add header to delete', ()=>{
|
||||
expect(testFn).not.toHaveBeenCalled();
|
||||
expect(setFn).not.toHaveBeenCalled();
|
||||
|
||||
request.delete = testFn;
|
||||
|
||||
requestMiddleware.delete('path');
|
||||
|
||||
expect(testFn).toHaveBeenCalledWith('path');
|
||||
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,7 @@ function parseBrewForStorage(brew, slot = 0) {
|
||||
title : brew.title,
|
||||
text : brew.text,
|
||||
style : brew.style,
|
||||
snippets : brew.snippets,
|
||||
version : brew.version,
|
||||
shareId : brew.shareId,
|
||||
savedAt : brew?.savedAt || new Date(),
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import react from "eslint-plugin-react";
|
||||
import jest from "eslint-plugin-jest";
|
||||
import globals from "globals";
|
||||
import localPlugin from "./eslint_plugins/index.js";
|
||||
|
||||
export default [{
|
||||
ignores: ["build/"]
|
||||
},
|
||||
{
|
||||
files : ['**/*.js', '**/*.jsx'],
|
||||
plugins : { react, jest },
|
||||
plugins : { react, jest, local: localPlugin },
|
||||
languageOptions : {
|
||||
ecmaVersion : "latest",
|
||||
sourceType : "module",
|
||||
@@ -46,9 +47,9 @@ export default [{
|
||||
"semi" : ["warn", "always"],
|
||||
|
||||
/** Whitespace **/
|
||||
"array-bracket-spacing" : ["warn", "never"],
|
||||
//"array-bracket-spacing" : ["warn", "never"],
|
||||
"arrow-spacing" : ["warn", { before: false, after: false }],
|
||||
"comma-spacing" : ["warn", { before: false, after: true }],
|
||||
//"comma-spacing" : ["warn", { before: false, after: true }],
|
||||
"indent" : ["warn", "tab", { MemberExpression: "off" }],
|
||||
"linebreak-style" : "off",
|
||||
"no-trailing-spaces" : "warn",
|
||||
@@ -65,7 +66,10 @@ export default [{
|
||||
"key-spacing" : ["warn", {
|
||||
multiLine : { beforeColon: true, afterColon: true, align: "colon" },
|
||||
singleLine : { beforeColon: false, afterColon: true }
|
||||
}]
|
||||
}],
|
||||
|
||||
"local/aligned-useState-pairs": "warn"
|
||||
|
||||
}
|
||||
}
|
||||
];
|
||||
7
eslint_plugins/index.js
Normal file
7
eslint_plugins/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import alignedUseStatePairs from './rules/aligned-useState-pairs.js';
|
||||
|
||||
export default {
|
||||
rules: {
|
||||
'aligned-useState-pairs': alignedUseStatePairs
|
||||
}
|
||||
};
|
||||
103
eslint_plugins/rules/aligned-useState-pairs.js
Normal file
103
eslint_plugins/rules/aligned-useState-pairs.js
Normal file
@@ -0,0 +1,103 @@
|
||||
export default {
|
||||
meta: {
|
||||
type: "layout",
|
||||
docs: {
|
||||
description: "Enforce alignment of adjacent useState variable pairs",
|
||||
},
|
||||
fixable: "whitespace",
|
||||
schema: [],
|
||||
},
|
||||
create(context) {
|
||||
const sourceCode = context.getSourceCode();
|
||||
const useStateDeclarations = [];
|
||||
|
||||
return {
|
||||
VariableDeclaration(node) {
|
||||
for (const decl of node.declarations) {
|
||||
const init = decl.init;
|
||||
if (
|
||||
init &&
|
||||
init.type === "CallExpression" &&
|
||||
init.callee.name === "useState" &&
|
||||
decl.id.type === "ArrayPattern"
|
||||
) {
|
||||
useStateDeclarations.push(decl);
|
||||
}
|
||||
}
|
||||
},
|
||||
"Program:exit"() {
|
||||
if (useStateDeclarations.length < 2) return;
|
||||
|
||||
// Sort by line number
|
||||
useStateDeclarations.sort(
|
||||
(a, b) => a.loc.start.line - b.loc.start.line
|
||||
);
|
||||
|
||||
// Group adjacent lines
|
||||
const groups = [];
|
||||
let currentGroup = [useStateDeclarations[0]];
|
||||
|
||||
for (let i = 1; i < useStateDeclarations.length; i++) {
|
||||
const prev = useStateDeclarations[i - 1];
|
||||
const curr = useStateDeclarations[i];
|
||||
|
||||
if (curr.loc.start.line === prev.loc.end.line + 1) {
|
||||
currentGroup.push(curr);
|
||||
} else {
|
||||
if (currentGroup.length > 1) groups.push(currentGroup);
|
||||
currentGroup = [curr];
|
||||
}
|
||||
}
|
||||
if (currentGroup.length > 1) groups.push(currentGroup);
|
||||
|
||||
// Analyze each group
|
||||
for (const group of groups) {
|
||||
const positions = group.map((decl) => {
|
||||
const text = sourceCode.getText(decl);
|
||||
const commaIndex = text.indexOf(",");
|
||||
const closingBracketIndex = text.lastIndexOf("]");
|
||||
return {
|
||||
node: decl,
|
||||
comma: commaIndex,
|
||||
closing: closingBracketIndex,
|
||||
};
|
||||
});
|
||||
|
||||
const maxComma = Math.max(...positions.map((p) => p.comma));
|
||||
const maxClosing = Math.max(
|
||||
...positions.map((p) => p.closing)
|
||||
);
|
||||
|
||||
for (const pos of positions) {
|
||||
if (
|
||||
pos.comma !== maxComma ||
|
||||
pos.closing !== maxClosing
|
||||
) {
|
||||
console.log(context);
|
||||
context.report({
|
||||
node: pos.node,
|
||||
message: "useState pair is not aligned with others in its block.",
|
||||
fix(fixer) {
|
||||
const text = sourceCode.getText(pos.node);
|
||||
const parts = text.match(/^\[\s*(.+?)\s*,\s*(.+?)\s*\]\s*=\s*useState\((.+)\)$/);
|
||||
if (!parts) return null;
|
||||
|
||||
const [_, left, right, value] = parts;
|
||||
|
||||
const paddedLeft = left.padEnd(maxComma - 1);
|
||||
const paddedRight = right.padEnd(maxClosing - maxComma - 2);
|
||||
const aligned = `[${paddedLeft}, ${paddedRight}] = useState(${value})`;
|
||||
return [
|
||||
fixer.replaceText(pos.node, aligned),
|
||||
fixer.insertTextBefore(pos.node.parent, ""),
|
||||
fixer.insertTextAfter(pos.node.parent, "")
|
||||
];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
4534
package-lock.json
generated
4534
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.19.0",
|
||||
"version": "3.19.3",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"npm": "^10.8.x",
|
||||
@@ -72,7 +72,7 @@
|
||||
"lines": 50
|
||||
},
|
||||
"server/homebrew.api.js": {
|
||||
"statements": 70,
|
||||
"statements": 60,
|
||||
"branches": 50,
|
||||
"functions": 65,
|
||||
"lines": 70
|
||||
@@ -84,24 +84,28 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.27.1",
|
||||
"@babel/plugin-transform-runtime": "^7.27.1",
|
||||
"@babel/preset-env": "^7.27.2",
|
||||
"@babel/plugin-transform-runtime": "^7.28.0",
|
||||
"@babel/preset-env": "^7.28.0",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@babel/runtime": "^7.27.1",
|
||||
"@googleapis/drive": "^12.1.0",
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||
"@googleapis/drive": "^13.0.1",
|
||||
"@sanity/diff-match-patch": "^3.2.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"core-js": "^3.42.0",
|
||||
"core-js": "^3.44.0",
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^5.1.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.2.0",
|
||||
"express-static-gzip": "3.0.0",
|
||||
"fflate": "^0.8.2",
|
||||
"fs-extra": "11.3.0",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
@@ -110,41 +114,41 @@
|
||||
"marked": "15.0.12",
|
||||
"marked-alignment-paragraphs": "^1.0.0",
|
||||
"marked-definition-lists": "^1.0.1",
|
||||
"marked-emoji": "^2.0.0",
|
||||
"marked-emoji": "^2.0.1",
|
||||
"marked-extended-tables": "^2.0.1",
|
||||
"marked-gfm-heading-id": "^4.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.2",
|
||||
"marked-nonbreaking-spaces": "^1.0.1",
|
||||
"marked-smartypants-lite": "^1.0.3",
|
||||
"marked-subsuper-text": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.14.3",
|
||||
"mongoose": "^8.16.3",
|
||||
"nanoid": "5.1.5",
|
||||
"nconf": "^0.13.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router": "^7.6.0",
|
||||
"romans": "^3.0.0",
|
||||
"react-router": "^7.6.3",
|
||||
"romans": "^3.1.0",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^10.2.1",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
|
||||
"written-number": "^0.11.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^3.1.2",
|
||||
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"@stylistic/stylelint-plugin": "^4.0.0",
|
||||
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-jest": "^29.0.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^30.1.3",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^16.19.1",
|
||||
"stylelint-config-recess-order": "^6.0.0",
|
||||
"stylelint-config-recommended": "^16.0.0",
|
||||
"supertest": "^7.1.1"
|
||||
"stylelint": "^16.24.0",
|
||||
"stylelint-config-recess-order": "^7.3.0",
|
||||
"stylelint-config-recommended": "^17.0.0",
|
||||
"supertest": "^7.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"codemirror/addon/selection/active-line.js",
|
||||
"codemirror/addon/hint/show-hint.js",
|
||||
"moment",
|
||||
"superagent"
|
||||
"superagent",
|
||||
"@sanity/diff-match-patch",
|
||||
"fflate"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -383,6 +383,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||
locale : req.brew.lang,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
@@ -404,6 +405,7 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
|
||||
renderer : req.brew.renderer,
|
||||
theme : req.brew.theme,
|
||||
tags : req.brew.tags,
|
||||
snippets : req.brew.snippets
|
||||
};
|
||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||
|
||||
@@ -485,8 +487,8 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||
.catch((err)=>{
|
||||
mongoCount = 0;
|
||||
console.log(err);
|
||||
return 0;
|
||||
});
|
||||
|
||||
data.accountDetails = {
|
||||
|
||||
66
server/forcessl.mw.spec.js
Normal file
66
server/forcessl.mw.spec.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import forceSSL from './forcessl.mw';
|
||||
|
||||
describe('Tests for ForceSSL middleware', ()=>{
|
||||
let originalEnv;
|
||||
let nextFn;
|
||||
|
||||
let req = {};
|
||||
let res = {};
|
||||
|
||||
beforeEach(()=>{
|
||||
originalEnv = process.env.NODE_ENV;
|
||||
nextFn = jest.fn();
|
||||
|
||||
req = {
|
||||
header : ()=>{ return 'http'; },
|
||||
get : ()=>{ return 'test'; },
|
||||
url : 'URL'
|
||||
};
|
||||
|
||||
res = {
|
||||
redirect : jest.fn()
|
||||
};
|
||||
});
|
||||
afterEach(()=>{
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not redirect when NODE_ENV is set to local', ()=>{
|
||||
process.env.NODE_ENV = 'local';
|
||||
|
||||
forceSSL(null, null, nextFn);
|
||||
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(nextFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not redirect when NODE_ENV is set to docker', ()=>{
|
||||
process.env.NODE_ENV = 'docker';
|
||||
|
||||
forceSSL(null, null, nextFn);
|
||||
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(nextFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect with 302 when header is not HTTPS and NODE_ENV is not local or docker', ()=>{
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
forceSSL(req, res, nextFn);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(302, 'https://testURL');
|
||||
expect(nextFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not redirect when header is HTTPS and NODE_ENV is not local or docker', ()=>{
|
||||
process.env.NODE_ENV = 'test';
|
||||
req.header = ()=>{ return 'https'; };
|
||||
|
||||
forceSSL(req, res, nextFn);
|
||||
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(nextFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -8,8 +8,10 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
||||
import yaml from 'js-yaml';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { nanoid } from 'nanoid';
|
||||
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
|
||||
import { md5 } from 'hash-wasm';
|
||||
import { splitTextStyleAndMetadata,
|
||||
brewSnippetsToJSON } from '../shared/helpers.js';
|
||||
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
||||
import checkClientVersion from './middleware/check-client-version.js';
|
||||
|
||||
|
||||
@@ -46,6 +48,20 @@ const api = {
|
||||
}
|
||||
id = id.slice(googleId.length);
|
||||
}
|
||||
|
||||
// ID Validation Checks
|
||||
// Homebrewery ID
|
||||
// Typically 12 characters, but the DB shows a range of 7 to 14 characters
|
||||
if(!id.match(/^[a-zA-Z0-9-_]{7,14}$/)){
|
||||
throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id };
|
||||
}
|
||||
// Google ID
|
||||
// Typically 33 characters, old format is 44 - always starts with a 1
|
||||
// Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable
|
||||
if(googleId && !googleId.match(/^1(?:[a-zA-Z0-9-_]{32,43})$/)){
|
||||
throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id };
|
||||
}
|
||||
|
||||
return { id, googleId };
|
||||
},
|
||||
//Get array of any of this user's brews tagged with `meta:theme`
|
||||
@@ -337,21 +353,52 @@ const api = {
|
||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||
const brewFromServer = req.brew;
|
||||
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
||||
splitTextStyleAndMetadata(brewFromServer);
|
||||
|
||||
if(brewFromServer?.version !== brewFromClient?.version){
|
||||
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
return res.status(409).send(JSON.stringify({ message: `The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
brewFromServer.text = brewFromServer.text.normalize('NFC');
|
||||
brewFromServer.hash = await md5(brewFromServer.text);
|
||||
|
||||
if(brewFromServer?.hash !== brewFromClient?.hash) {
|
||||
console.log(`Hash mismatch on brew ${brewFromClient.editId}`);
|
||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
return res.status(409).send(JSON.stringify({ message: `The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.` }));
|
||||
}
|
||||
|
||||
try {
|
||||
const patches = parsePatch(brewFromClient.patches);
|
||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
|
||||
if(patchedResult != brewFromClient.text)
|
||||
throw("Patches did not apply cleanly, text mismatch detected");
|
||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||
} catch (err) {
|
||||
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
console.error('Failed to apply patches:', {
|
||||
//patches : brewFromClient.patches,
|
||||
brewId : brewFromClient.editId || 'unknown',
|
||||
error : err
|
||||
});
|
||||
// While running in parallel, don't throw the error upstream.
|
||||
// throw err; // rethrow to preserve the 500 behavior
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
const googleId = brew.googleId;
|
||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||
let afterSave = async ()=>true;
|
||||
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
if(brew.googleId && removeFromGoogle) {
|
||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||
afterSave = async ()=>{
|
||||
@@ -412,6 +459,8 @@ const api = {
|
||||
const after = await afterSave();
|
||||
if(!after) return;
|
||||
|
||||
saved.textBin = undefined; // Remove textBin from the saved object to save bandwidth
|
||||
|
||||
res.status(200).send(saved);
|
||||
},
|
||||
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||
@@ -482,10 +531,10 @@ const api = {
|
||||
};
|
||||
|
||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', false)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
export default api;
|
||||
export default api;
|
||||
|
||||
@@ -99,18 +99,87 @@ describe('Tests for api', ()=>{
|
||||
expect(googleId).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw if id is too short', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcd'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
};
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '11', brewId: 'abcd', message: 'Invalid ID', name: 'ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should return id and google id from request body', ()=>{
|
||||
const { id, googleId } = api.getId({
|
||||
params : {
|
||||
id : 'abcdefgh'
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '12345'
|
||||
googleId : '123456789012345678901234567890123'
|
||||
}
|
||||
});
|
||||
|
||||
expect(id).toEqual('abcdefgh');
|
||||
expect(googleId).toEqual('12345');
|
||||
expect(id).toEqual('abcdefghijkl');
|
||||
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||
});
|
||||
|
||||
it('should throw invalid - google id right length but does not match pattern', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '012345678901234567890123456789012'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should throw invalid - google id too short (32 char)', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '12345678901234567890123456789012'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should throw invalid - google id too long (45 char)', ()=>{
|
||||
let err;
|
||||
try {
|
||||
api.getId({
|
||||
params : {
|
||||
id : 'abcdefghijkl'
|
||||
},
|
||||
body : {
|
||||
googleId : '123456789012345678901234567890123456789012345'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
|
||||
expect(err).toEqual({ HBErrorCode: '12', brewId: 'abcdefghijkl', message: 'Invalid ID', name: 'Google ID Error', status: 404 });
|
||||
});
|
||||
|
||||
it('should return 12-char id and google id from params', ()=>{
|
||||
@@ -1052,4 +1121,83 @@ brew`);
|
||||
expect(testBrew.tags).toEqual(['tag a']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBrew', ()=>{
|
||||
it('should return error on version mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1 };
|
||||
const brewFromServer = { version: 1000, text: '' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server version is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
it('should return error on hash mismatch', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '1234' };
|
||||
const brewFromServer = { version: 1, text: 'test' };
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(req.brew.hash).toBe('098f6bcd4621d373cade4e832627b4f6');
|
||||
expect(res.status).toHaveBeenCalledWith(409);
|
||||
expect(res.send).toHaveBeenCalledWith('{\"message\":\"The server copy is out of sync with the saved brew. Please save your changes elsewhere, refresh, and try again.\"}');
|
||||
});
|
||||
|
||||
// Commenting this one out for now, since we are no longer throwing this error while we monitor
|
||||
// it('should return error on applying patches', async ()=>{
|
||||
// const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: 'not a valid patch string' };
|
||||
// const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
// const req = {
|
||||
// brew : brewFromServer,
|
||||
// body : brewFromClient,
|
||||
// };
|
||||
|
||||
// let err;
|
||||
// try {
|
||||
// await api.updateBrew(req, res);
|
||||
// } catch (e) {
|
||||
// err = e;
|
||||
// }
|
||||
|
||||
// expect(err).toEqual(Error('Invalid patch string: not a valid patch string'));
|
||||
// });
|
||||
|
||||
it('should save brew, no ID', async ()=>{
|
||||
const brewFromClient = { version: 1, hash: '098f6bcd4621d373cade4e832627b4f6', patches: '' };
|
||||
const brewFromServer = { version: 1, text: 'test', title: 'Test Title', description: 'Test Description' };
|
||||
|
||||
model.save = jest.fn((brew)=>{return brew;});
|
||||
|
||||
const req = {
|
||||
brew : brewFromServer,
|
||||
body : brewFromClient,
|
||||
query : { saveToGoogle: false, removeFromGoogle: false }
|
||||
};
|
||||
|
||||
await api.updateBrew(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
_id : '1',
|
||||
description : 'Test Description',
|
||||
hash : '098f6bcd4621d373cade4e832627b4f6',
|
||||
title : 'Test Title',
|
||||
version : 2
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,21 +5,16 @@ import config from './config.js';
|
||||
const generateAccessToken = (account)=>{
|
||||
const payload = account;
|
||||
|
||||
// When the token was issued
|
||||
payload.issued = (new Date());
|
||||
// Which service issued the Token
|
||||
payload.issuer = config.get('authentication_token_issuer');
|
||||
// Which service is the token intended for
|
||||
payload.audience = config.get('authentication_token_audience');
|
||||
// The signing key for signing the token
|
||||
payload.issued = (new Date()); // When the token was issued
|
||||
payload.issuer = config.get('authentication_token_issuer'); // Which service issued the Token
|
||||
payload.audience = config.get('authentication_token_audience'); // Which service is the token intended for
|
||||
const secret = config.get('authentication_token_secret'); // The signing key for signing the token
|
||||
|
||||
delete payload.password;
|
||||
delete payload._id;
|
||||
|
||||
const secret = config.get('authentication_token_secret');
|
||||
|
||||
const token = jwt.encode(payload, secret);
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
export default generateAccessToken;
|
||||
export default generateAccessToken;
|
||||
|
||||
27
server/token.spec.js
Normal file
27
server/token.spec.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { expect, jest } from '@jest/globals';
|
||||
import config from './config.js';
|
||||
|
||||
import generateAccessToken from './token';
|
||||
|
||||
describe('Tests for Token', ()=>{
|
||||
it('Get token', ()=>{
|
||||
|
||||
// Mock the Config module, so we aren't grabbing actual secrets for testing
|
||||
jest.mock('./config.js');
|
||||
config.get = jest.fn((param)=>{
|
||||
// The requested key name will be reflected to the output
|
||||
return param;
|
||||
});
|
||||
|
||||
const account = {};
|
||||
|
||||
const token = generateAccessToken(account);
|
||||
|
||||
// If these tests fail, the config mock has failed
|
||||
expect(account).toHaveProperty('issuer', 'authentication_token_issuer');
|
||||
expect(account).toHaveProperty('audience', 'authentication_token_audience');
|
||||
|
||||
// Because the inputs are fixed, this JWT key should be static
|
||||
expect(typeof token).toBe('string');
|
||||
});
|
||||
});
|
||||
@@ -116,32 +116,62 @@ const printCurrentBrew = ()=>{
|
||||
}
|
||||
};
|
||||
|
||||
const fetchThemeBundle = async (obj, renderer, theme)=>{
|
||||
const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
|
||||
if(!renderer || !theme) return;
|
||||
const res = await request
|
||||
.get(`/api/theme/${renderer}/${theme}`)
|
||||
.catch((err)=>{
|
||||
obj.setState({ error: err });
|
||||
setError(err)
|
||||
});
|
||||
if(!res) {
|
||||
obj.setState((prevState)=>({
|
||||
...prevState,
|
||||
themeBundle : {}
|
||||
}));
|
||||
setThemeBundle({});
|
||||
return;
|
||||
}
|
||||
const themeBundle = res.body;
|
||||
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
||||
obj.setState((prevState)=>({
|
||||
...prevState,
|
||||
themeBundle : themeBundle,
|
||||
error : null
|
||||
}));
|
||||
setThemeBundle(themeBundle);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
||||
const clientText = clientTextRaw?.normalize('NFC') || '';
|
||||
const serverText = serverTextRaw?.normalize('NFC') || '';
|
||||
|
||||
const clientBuffer = Buffer.from(clientText, 'utf8');
|
||||
const serverBuffer = Buffer.from(serverText, 'utf8');
|
||||
|
||||
if (clientBuffer.equals(serverBuffer)) {
|
||||
console.log(`✅ ${label} text matches byte-for-byte.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(`❗${label} text mismatch detected.`);
|
||||
console.log(`Client length: ${clientBuffer.length}`);
|
||||
console.log(`Server length: ${serverBuffer.length}`);
|
||||
|
||||
// Byte-level diff
|
||||
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
|
||||
if (clientBuffer[i] !== serverBuffer[i]) {
|
||||
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Char-level diff
|
||||
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
||||
if (clientText[i] !== serverText[i]) {
|
||||
console.log(`Char mismatch at index ${i}:`);
|
||||
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
splitTextStyleAndMetadata,
|
||||
printCurrentBrew,
|
||||
fetchThemeBundle,
|
||||
brewSnippetsToJSON
|
||||
brewSnippetsToJSON,
|
||||
debugTextMismatch
|
||||
};
|
||||
|
||||
@@ -185,7 +185,7 @@ const mustacheSpans = {
|
||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
|
||||
const match = completeSpan.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -242,7 +242,7 @@ const mustacheDivs = {
|
||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const match = completeBlock.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -297,7 +297,7 @@ const mustacheInjectInline = {
|
||||
level : 'inline',
|
||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
@@ -343,7 +343,7 @@ const mustacheInjectBlock = {
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
|
||||
@@ -4,6 +4,17 @@ require('jsdom-global')();
|
||||
|
||||
import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
|
||||
|
||||
test('Exit if no document', function() {
|
||||
const doc = document;
|
||||
document = undefined;
|
||||
|
||||
const result = safeHTML('');
|
||||
|
||||
document = doc;
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
test('Javascript via href', function() {
|
||||
const source = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
|
||||
const rendered = safeHTML(source);
|
||||
|
||||
BIN
themes/V3/UnearthedArcana/dropdownPreview.png
Normal file
BIN
themes/V3/UnearthedArcana/dropdownPreview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
themes/V3/UnearthedArcana/dropdownTexture.png
Normal file
BIN
themes/V3/UnearthedArcana/dropdownTexture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 236 B |
6
themes/V3/UnearthedArcana/settings.json
Normal file
6
themes/V3/UnearthedArcana/settings.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name" : "UnearthedArcana",
|
||||
"renderer" : "V3",
|
||||
"baseTheme" : false,
|
||||
"baseSnippets" : false
|
||||
}
|
||||
38
themes/V3/UnearthedArcana/style.less
Normal file
38
themes/V3/UnearthedArcana/style.less
Normal file
@@ -0,0 +1,38 @@
|
||||
@import (less) './themes/fonts/5e/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
|
||||
:root {
|
||||
//Colors
|
||||
--HB_Color_Background : #FFFFFF; // White
|
||||
--HB_Color_WatercolorStain : #000000; // Black
|
||||
}
|
||||
|
||||
.page {
|
||||
font-family: Cambria,Georgia,serif;
|
||||
font-size: 14px;
|
||||
h1, h2, h3, h4 {
|
||||
font-variant: small-caps;
|
||||
font-weight: normal;
|
||||
}
|
||||
h1 {
|
||||
column-span: all;
|
||||
-webkit-column-span: all;
|
||||
font-size: 40px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
41
themes/assets/naturalCritLogoBlack.svg
Normal file
41
themes/assets/naturalCritLogoBlack.svg
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 94.65 94.6"
|
||||
version="1.1"
|
||||
id="svg11"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs4">
|
||||
<style
|
||||
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||
</defs>
|
||||
<title
|
||||
id="title6">NaturalCritLogo</title>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2"
|
||||
style="fill:#000000">
|
||||
<g
|
||||
id="base"
|
||||
style="fill:#000000">
|
||||
<path
|
||||
id="D20"
|
||||
class="cls-1"
|
||||
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||
style="fill:#000000;fill-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
<metadata
|
||||
id="metadata1">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:title>NaturalCritLogo</dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -35,6 +35,13 @@
|
||||
"baseTheme": "Blank",
|
||||
"baseSnippets": "5ePHB",
|
||||
"path": "Journal"
|
||||
},
|
||||
"UnearthedArcana": {
|
||||
"name": "UnearthedArcana",
|
||||
"renderer": "V3",
|
||||
"baseTheme": false,
|
||||
"baseSnippets": false,
|
||||
"path": "UnearthedArcana"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user