mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-03 04:02:44 +00:00
Merge branch 'master' into Issue_2563
This commit is contained in:
30
changelog.md
30
changelog.md
@@ -84,6 +84,34 @@ pre {
|
|||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Monday 7/29/2024 - v3.14.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### abquintic, calculuschild
|
||||||
|
|
||||||
|
* [x] Alternative Brew Themes, including importing other brews as a base theme.
|
||||||
|
|
||||||
|
- In the :fas_circle_info: **Properties** menu, find the new {{openSans **THEME**}} dropdown. It lists Brew Themes, including a new **Blank** theme as a simpler basis for custom styling.
|
||||||
|
- Brews tagged with `meta:theme` will appear in the Brew Themes list. Selecting one loads its :fas_paintbrush: **Style** tab contents as the CSS basis for the current brew, allowing one brew to style multiple documents.
|
||||||
|
- Brews with `meta:theme` can also select their own Theme, i.e. layering Themes on top of each other.
|
||||||
|
- The next goal is to make **Published** Themes shareable between users.
|
||||||
|
|
||||||
|
|
||||||
|
Fixes issues [#1899](https://github.com/naturalcrit/homebrewery/issues/1899), [#3085](https://github.com/naturalcrit/homebrewery/issues/3085)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix Drop-cap font becoming corrupted when Bold
|
||||||
|
|
||||||
|
Fixes issues [#3551](https://github.com/naturalcrit/homebrewery/issues/3551)
|
||||||
|
|
||||||
|
* [x] Fixes to UI styling
|
||||||
|
|
||||||
|
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
### Saturday 6/7/2024 - v3.13.1
|
### Saturday 6/7/2024 - v3.13.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -131,8 +159,6 @@ Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
|
|||||||
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
|
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\column
|
|
||||||
|
|
||||||
### Monday 18/3/2024 - v3.12.0
|
### Monday 18/3/2024 - v3.12.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ const { printCurrentBrew } = require('../../../shared/helpers.js');
|
|||||||
const DOMPurify = require('dompurify');
|
const DOMPurify = require('dompurify');
|
||||||
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
|
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
|
||||||
|
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
@@ -57,6 +55,7 @@ const BrewRenderer = (props)=>{
|
|||||||
lang : '',
|
lang : '',
|
||||||
errors : [],
|
errors : [],
|
||||||
currentEditorPage : 0,
|
currentEditorPage : 0,
|
||||||
|
themeBundle : {},
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,10 +124,9 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStyle = ()=>{
|
const renderStyle = ()=>{
|
||||||
if(!props.style) return;
|
|
||||||
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
|
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
|
||||||
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
|
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `${themeStyles} \n\n <style> ${cleanStyle} </style>` }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPage = (pageText, index)=>{
|
const renderPage = (pageText, index)=>{
|
||||||
@@ -188,10 +186,6 @@ const BrewRenderer = (props)=>{
|
|||||||
document.dispatchEvent(new MouseEvent('click'));
|
document.dispatchEvent(new MouseEvent('click'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
|
|
||||||
const themePath = props.theme ?? '5ePHB';
|
|
||||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*render dummy page while iFrame is mounting.*/}
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
@@ -220,13 +214,6 @@ const BrewRenderer = (props)=>{
|
|||||||
onKeyDown={handleControlKeys}
|
onKeyDown={handleControlKeys}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
style={{ height: state.height }}>
|
style={{ height: state.height }}>
|
||||||
|
|
||||||
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
|
|
||||||
{baseThemePath &&
|
|
||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>
|
|
||||||
}
|
|
||||||
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type='text/css' rel='stylesheet'/>
|
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
|
|||||||
@@ -381,7 +381,8 @@ const Editor = createClass({
|
|||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
onChange={this.props.onMetaChange}
|
onChange={this.props.onMetaChange}
|
||||||
reportError={this.props.reportError}/>
|
reportError={this.props.reportError}
|
||||||
|
userThemes={this.props.userThemes}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -424,6 +425,7 @@ const Editor = createClass({
|
|||||||
historySize={this.historySize()}
|
historySize={this.historySize()}
|
||||||
currentEditorTheme={this.state.editorTheme}
|
currentEditorTheme={this.state.editorTheme}
|
||||||
updateEditorTheme={this.updateEditorTheme}
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
|
snippetBundle={this.props.snippetBundle}
|
||||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const Combobox = require('client/components/combobox.jsx');
|
const Combobox = require('client/components/combobox.jsx');
|
||||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
const validations = require('./validations.js');
|
const validations = require('./validations.js');
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ const MetadataEditor = createClass({
|
|||||||
if(renderer == 'legacy')
|
if(renderer == 'legacy')
|
||||||
this.props.metadata.theme = '5ePHB';
|
this.props.metadata.theme = '5ePHB';
|
||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata, 'renderer');
|
||||||
},
|
},
|
||||||
handlePublish : function(val){
|
handlePublish : function(val){
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
@@ -110,7 +111,7 @@ const MetadataEditor = createClass({
|
|||||||
handleTheme : function(theme){
|
handleTheme : function(theme){
|
||||||
this.props.metadata.renderer = theme.renderer;
|
this.props.metadata.renderer = theme.renderer;
|
||||||
this.props.metadata.theme = theme.path;
|
this.props.metadata.theme = theme.path;
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
},
|
},
|
||||||
|
|
||||||
handleLanguage : function(languageCode){
|
handleLanguage : function(languageCode){
|
||||||
@@ -191,37 +192,41 @@ const MetadataEditor = createClass({
|
|||||||
renderThemeDropdown : function(){
|
renderThemeDropdown : function(){
|
||||||
if(!global.enable_themes) return;
|
if(!global.enable_themes) return;
|
||||||
|
|
||||||
|
const mergedThemes = _.merge(Themes, this.props.userThemes);
|
||||||
|
|
||||||
const listThemes = (renderer)=>{
|
const listThemes = (renderer)=>{
|
||||||
return _.map(_.values(Themes[renderer]), (theme)=>{
|
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
|
||||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||||
{`${theme.renderer} : ${theme.name}`}
|
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||||
|
{theme.author ?? renderer} : {theme.name}
|
||||||
|
<div className='texture-container'>
|
||||||
|
<img src={texture}/>
|
||||||
|
</div>
|
||||||
<div className='preview'>
|
<div className='preview'>
|
||||||
<h6>{`${theme.name}`} preview</h6>
|
<h6>{theme.name} preview</h6>
|
||||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
|
<img src={preview}/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
|
const currentRenderer = this.props.metadata.renderer;
|
||||||
|
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
||||||
|
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
||||||
let dropdown;
|
let dropdown;
|
||||||
|
|
||||||
if(this.props.metadata.renderer == 'legacy') {
|
if(currentRenderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
<Nav.dropdown className='disabled value' trigger='disabled'>
|
||||||
<div>
|
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
||||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
|
||||||
</div>
|
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='value' trigger='click'>
|
<Nav.dropdown className='value' trigger='click'>
|
||||||
<div>
|
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
||||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
|
||||||
</div>
|
{listThemes(currentRenderer)}
|
||||||
{/*listThemes('Legacy')*/}
|
|
||||||
{listThemes('V3')}
|
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,13 @@
|
|||||||
color : white;
|
color : white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navDropdown .item > p {
|
||||||
|
width: 45%;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 1.1em;
|
||||||
|
}
|
||||||
.navDropdown {
|
.navDropdown {
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -230,14 +237,23 @@
|
|||||||
&:hover > .preview {
|
&:hover > .preview {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
>img {
|
.texture-container {
|
||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
position: absolute;
|
||||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
width: 100%;
|
||||||
position : absolute;
|
height: 100%;
|
||||||
right : 0;
|
min-height: 100%;
|
||||||
top : 0px;
|
top: 0;
|
||||||
width : 50%;
|
left: 0;
|
||||||
height : 100%;
|
overflow: hidden;
|
||||||
|
> img {
|
||||||
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
|
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
|
position : absolute;
|
||||||
|
right : 0;
|
||||||
|
top : 0px;
|
||||||
|
width : 50%;
|
||||||
|
min-height : 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ const _ = require('lodash');
|
|||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
//Import all themes
|
//Import all themes
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
|
||||||
|
|
||||||
const ThemeSnippets = {};
|
const ThemeSnippets = {};
|
||||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||||
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
||||||
@@ -40,7 +37,8 @@ const Snippetbar = createClass({
|
|||||||
foldCode : ()=>{},
|
foldCode : ()=>{},
|
||||||
unfoldCode : ()=>{},
|
unfoldCode : ()=>{},
|
||||||
updateEditorTheme : ()=>{},
|
updateEditorTheme : ()=>{},
|
||||||
cursorPos : {}
|
cursorPos : {},
|
||||||
|
snippetBundle : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -53,21 +51,15 @@ const Snippetbar = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : async function() {
|
componentDidMount : async function() {
|
||||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
const snippets = this.compileSnippets();
|
||||||
const themePath = this.props.theme ?? '5ePHB';
|
|
||||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
|
||||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : snippets
|
snippets : snippets
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : async function(prevProps) {
|
componentDidUpdate : async function(prevProps) {
|
||||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
|
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
||||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
const snippets = this.compileSnippets();
|
||||||
const themePath = this.props.theme ?? '5ePHB';
|
|
||||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
|
||||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : snippets
|
snippets : snippets
|
||||||
});
|
});
|
||||||
@@ -75,26 +67,26 @@ const Snippetbar = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
mergeCustomizer : function(valueA, valueB, key) {
|
mergeCustomizer : function(oldValue, newValue, key) {
|
||||||
if(key == 'snippets') {
|
if(key == 'snippets') {
|
||||||
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||||
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
compileSnippets : function(rendererPath, themePath, snippets) {
|
compileSnippets : function() {
|
||||||
let compiledSnippets = snippets;
|
let compiledSnippets = [];
|
||||||
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
|
|
||||||
|
|
||||||
const objB = _.keyBy(compiledSnippets, 'groupName');
|
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||||
|
|
||||||
if(baseSnippetsPath) {
|
for (let snippets of this.props.snippetBundle) {
|
||||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
|
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
snippets = ThemeSnippets[snippets];
|
||||||
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
|
|
||||||
} else {
|
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
|
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
|
||||||
|
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||||
}
|
}
|
||||||
return compiledSnippets;
|
return compiledSnippets;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,10 +66,10 @@ const Homebrew = createClass({
|
|||||||
<Router location={this.props.url}>
|
<Router location={this.props.url}>
|
||||||
<div className='homebrew'>
|
<div className='homebrew'>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
<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='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/new/:id' element={<WithRoute el={NewPage} 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}/>} />
|
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
|
|||||||
@@ -104,6 +104,18 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</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>;
|
||||||
|
}
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@
|
|||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
|
.lowercase {
|
||||||
|
text-transform : none;
|
||||||
|
}
|
||||||
a{
|
a{
|
||||||
color : @teal;
|
color : @teal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,11 +119,12 @@
|
|||||||
text-align : center;
|
text-align : center;
|
||||||
a{
|
a{
|
||||||
.animate(opacity);
|
.animate(opacity);
|
||||||
display : block;
|
display : block;
|
||||||
margin : 8px 0px;
|
margin : 8px 0px;
|
||||||
opacity : 0.6;
|
opacity : 0.6;
|
||||||
font-size : 1.3em;
|
font-size : 1.3em;
|
||||||
color : white;
|
color : white;
|
||||||
|
text-decoration : unset;
|
||||||
&:hover{
|
&:hover{
|
||||||
opacity : 1;
|
opacity : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const LockNotification = require('./lockNotification/lockNotification.jsx');
|
|||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../googleDrive.svg');
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
@@ -55,7 +55,8 @@ const EditPage = createClass({
|
|||||||
autoSaveWarning : false,
|
autoSaveWarning : false,
|
||||||
unsavedTime : new Date(),
|
unsavedTime : new Date(),
|
||||||
currentEditorPage : 0,
|
currentEditorPage : 0,
|
||||||
displayLockMessage : this.props.brew.lock || false
|
displayLockMessage : this.props.brew.lock || false,
|
||||||
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -87,6 +88,8 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -130,7 +133,10 @@ const EditPage = createClass({
|
|||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(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);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : {
|
brew : {
|
||||||
...prevState.brew,
|
...prevState.brew,
|
||||||
@@ -138,7 +144,6 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
isPending : true,
|
isPending : true,
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hasChanges : function(){
|
hasChanges : function(){
|
||||||
@@ -406,12 +411,15 @@ const EditPage = createClass({
|
|||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
reportError={this.errorReported}
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
|
userThemes={this.props.userThemes}
|
||||||
|
snippetBundle={this.state.themeBundle.snippets}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.state.brew.text}
|
text={this.state.brew.text}
|
||||||
style={this.state.brew.style}
|
style={this.state.brew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
theme={this.state.brew.theme}
|
theme={this.state.brew.theme}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
errors={this.state.htmlErrors}
|
errors={this.state.htmlErrors}
|
||||||
lang={this.state.brew.lang}
|
lang={this.state.brew.lang}
|
||||||
currentEditorPage={this.state.currentEditorPage}
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
|||||||
@@ -136,6 +136,19 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Theme load error
|
||||||
|
'09' : dedent`
|
||||||
|
## No Homebrewery theme document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Homebrewery document. It was likely deleted by
|
||||||
|
its owner.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Requested access:** ${props.brew.accessType}
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
// Brew locked by Administrators error
|
// Brew locked by Administrators error
|
||||||
'100' : dedent`
|
'100' : dedent`
|
||||||
## This brew has been locked.
|
## This brew has been locked.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
@@ -34,12 +35,17 @@ const HomePage = createClass({
|
|||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text,
|
welcomeText : this.props.brew.text,
|
||||||
error : undefined,
|
error : undefined,
|
||||||
currentEditorPage : 0
|
currentEditorPage : 0,
|
||||||
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
editor : React.createRef(null),
|
editor : React.createRef(null),
|
||||||
|
|
||||||
|
componentDidMount : function() {
|
||||||
|
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||||
|
},
|
||||||
|
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(this.state.brew)
|
||||||
@@ -95,6 +101,7 @@ const HomePage = createClass({
|
|||||||
style={this.state.brew.style}
|
style={this.state.brew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
currentEditorPage={this.state.currentEditorPage}
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const Editor = require('../../editor/editor.jsx');
|
|||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
@@ -44,7 +44,8 @@ const NewPage = createClass({
|
|||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
error : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text),
|
htmlErrors : Markdown.validate(brew.text),
|
||||||
currentEditorPage : 0
|
currentEditorPage : 0,
|
||||||
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -77,6 +78,8 @@ const NewPage = createClass({
|
|||||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
if(brew.style)
|
if(brew.style)
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
@@ -122,7 +125,10 @@ const NewPage = createClass({
|
|||||||
localStorage.setItem(STYLEKEY, style);
|
localStorage.setItem(STYLEKEY, style);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(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);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, ...metadata },
|
brew : { ...prevState.brew, ...metadata },
|
||||||
}), ()=>{
|
}), ()=>{
|
||||||
@@ -142,8 +148,6 @@ const NewPage = createClass({
|
|||||||
isSaving : true
|
isSaving : true
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('saving new brew');
|
|
||||||
|
|
||||||
let brew = this.state.brew;
|
let brew = this.state.brew;
|
||||||
// Split out CSS to Style if CSS codefence exists
|
// Split out CSS to Style if CSS codefence exists
|
||||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||||
@@ -153,12 +157,10 @@ const NewPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
|
||||||
this.setState({ isSaving: false, error: err });
|
this.setState({ isSaving: false, error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
@@ -214,12 +216,14 @@ const NewPage = createClass({
|
|||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
|
userThemes={this.props.userThemes}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.state.brew.text}
|
text={this.state.brew.text}
|
||||||
style={this.state.brew.style}
|
style={this.state.brew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
theme={this.state.brew.theme}
|
theme={this.state.brew.theme}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
errors={this.state.htmlErrors}
|
errors={this.state.htmlErrors}
|
||||||
lang={this.state.brew.lang}
|
lang={this.state.brew.lang}
|
||||||
currentEditorPage={this.state.currentEditorPage}
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
|
|||||||
@@ -12,18 +12,26 @@ const Account = require('../../navbar/account.navitem.jsx');
|
|||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = createClass({
|
||||||
displayName : 'SharePage',
|
displayName : 'SharePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : DEFAULT_BREW_LOAD
|
brew : DEFAULT_BREW_LOAD,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
themeBundle : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
|
|
||||||
|
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -99,6 +107,7 @@ const SharePage = createClass({
|
|||||||
style={this.props.brew.style}
|
style={this.props.brew.style}
|
||||||
renderer={this.props.brew.renderer}
|
renderer={this.props.brew.renderer}
|
||||||
theme={this.props.brew.theme}
|
theme={this.props.brew.theme}
|
||||||
|
themeBundle={this.state.themeBundle}
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"secret" : "secret",
|
"secret" : "secret",
|
||||||
"web_port" : 8000,
|
"web_port" : 8000,
|
||||||
"enable_v3" : true,
|
"enable_v3" : true,
|
||||||
|
"enable_themes" : true,
|
||||||
"local_environments" : ["docker", "local"],
|
"local_environments" : ["docker", "local"],
|
||||||
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.13.1",
|
"version": "3.14.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
"node": "^20.8.x"
|
"node": "^20.8.x"
|
||||||
@@ -22,7 +22,8 @@
|
|||||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "jest --runInBand",
|
"test": "jest --runInBand",
|
||||||
"test:api-unit": "jest server/*.spec.js --verbose",
|
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
|
||||||
|
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
@@ -56,15 +57,15 @@
|
|||||||
],
|
],
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"global": {
|
"global": {
|
||||||
"statements": 25,
|
"statements": 50,
|
||||||
"branches": 10,
|
"branches": 40,
|
||||||
"functions": 22,
|
"functions": 40,
|
||||||
"lines": 25
|
"lines": 50
|
||||||
},
|
},
|
||||||
"server/homebrew.api.js": {
|
"server/homebrew.api.js": {
|
||||||
"statements": 65,
|
"statements": 70,
|
||||||
"branches": 50,
|
"branches": 50,
|
||||||
"functions": 60,
|
"functions": 65,
|
||||||
"lines": 70
|
"lines": 70
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const yaml = require('js-yaml');
|
|||||||
const app = express();
|
const app = express();
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
|
|
||||||
const { homebrewApi, getBrew } = require('./homebrew.api.js');
|
const { homebrewApi, getBrew, getUsersBrewThemes } = require('./homebrew.api.js');
|
||||||
const GoogleActions = require('./googleActions.js');
|
const GoogleActions = require('./googleActions.js');
|
||||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||||
const sanitizeFilename = require('sanitize-filename');
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
@@ -81,7 +81,8 @@ app.get('/robots.txt', (req, res)=>{
|
|||||||
app.get('/', (req, res, next)=>{
|
app.get('/', (req, res, next)=>{
|
||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeText,
|
text : welcomeText,
|
||||||
renderer : 'V3'
|
renderer : 'V3',
|
||||||
|
theme : '5ePHB'
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -97,7 +98,8 @@ app.get('/', (req, res, next)=>{
|
|||||||
app.get('/legacy', (req, res, next)=>{
|
app.get('/legacy', (req, res, next)=>{
|
||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeTextLegacy,
|
text : welcomeTextLegacy,
|
||||||
renderer : 'legacy'
|
renderer : 'legacy',
|
||||||
|
theme : '5ePHB'
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -265,9 +267,11 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
|
|
||||||
|
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
@@ -279,10 +283,10 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
|||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||||
return next();
|
return next();
|
||||||
});
|
}));
|
||||||
|
|
||||||
//New Page
|
//New Page from ID
|
||||||
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
|
||||||
sanitizeBrew(req.brew, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
const brew = {
|
const brew = {
|
||||||
@@ -292,17 +296,31 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
style : req.brew.style,
|
style : req.brew.style,
|
||||||
renderer : req.brew.renderer,
|
renderer : req.brew.renderer,
|
||||||
theme : req.brew.theme,
|
theme : req.brew.theme,
|
||||||
tags : req.brew.tags
|
tags : req.brew.tags,
|
||||||
};
|
};
|
||||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
|
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : 'New',
|
title : 'New',
|
||||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||||
};
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
}));
|
||||||
|
|
||||||
|
//New Page
|
||||||
|
app.get('/new', asyncHandler(async(req, res, next)=>{
|
||||||
|
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'New',
|
||||||
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||||
|
};
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
//Share Page
|
//Share Page
|
||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
@@ -418,7 +436,8 @@ const renderPage = async (req, res)=>{
|
|||||||
enable_v3 : config.get('enable_v3'),
|
enable_v3 : config.get('enable_v3'),
|
||||||
enable_themes : config.get('enable_themes'),
|
enable_themes : config.get('enable_themes'),
|
||||||
config : configuration,
|
config : configuration,
|
||||||
ogMeta : req.ogMeta
|
ogMeta : req.ogMeta,
|
||||||
|
userThemes : req.userThemes
|
||||||
};
|
};
|
||||||
const title = req.brew ? req.brew.title : '';
|
const title = req.brew ? req.brew.title : '';
|
||||||
const page = await templateFn('homebrew', title, props)
|
const page = await templateFn('homebrew', title, props)
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
|
|||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
|
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||||
|
|
||||||
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||||
|
|
||||||
|
const Themes = require('../themes/themes.json');
|
||||||
|
|
||||||
|
const isStaticTheme = (renderer, themeName)=>{
|
||||||
|
return Themes[renderer]?.[themeName] !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
// const getTopBrews = (cb) => {
|
// const getTopBrews = (cb) => {
|
||||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||||
// cb(brews);
|
// cb(brews);
|
||||||
@@ -37,6 +44,40 @@ const api = {
|
|||||||
}
|
}
|
||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
},
|
},
|
||||||
|
//Get array of any of this user's brews tagged with `meta:theme`
|
||||||
|
getUsersBrewThemes : async (username)=>{
|
||||||
|
const fields = [
|
||||||
|
'title',
|
||||||
|
'tags',
|
||||||
|
'shareId',
|
||||||
|
'thumbnail',
|
||||||
|
'textBin',
|
||||||
|
'text',
|
||||||
|
'authors',
|
||||||
|
'renderer'
|
||||||
|
];
|
||||||
|
|
||||||
|
const userThemes = {};
|
||||||
|
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
||||||
|
|
||||||
|
if(brews) {
|
||||||
|
for (const brew of brews) {
|
||||||
|
userThemes[brew.renderer] ??= {};
|
||||||
|
userThemes[brew.renderer][brew.shareId] = {
|
||||||
|
name : brew.title,
|
||||||
|
renderer : brew.renderer,
|
||||||
|
baseTheme : brew.theme,
|
||||||
|
baseSnippets : false,
|
||||||
|
author : brew.authors[0],
|
||||||
|
path : brew.shareId,
|
||||||
|
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userThemes;
|
||||||
|
},
|
||||||
getBrew : (accessType, stubOnly = false)=>{
|
getBrew : (accessType, stubOnly = false)=>{
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
return async (req, res, next)=>{
|
return async (req, res, next)=>{
|
||||||
@@ -209,6 +250,62 @@ const api = {
|
|||||||
|
|
||||||
res.status(200).send(saved);
|
res.status(200).send(saved);
|
||||||
},
|
},
|
||||||
|
getThemeBundle : async(req, res)=>{
|
||||||
|
/*
|
||||||
|
getThemeBundle: Collects the theme and all parent themes
|
||||||
|
returns an object containing an array of css, in render order, and an array
|
||||||
|
of snippets ( currently empty )
|
||||||
|
Important parameter members:
|
||||||
|
req.params.id: This is the shareId ( User theme ) or name ( static theme )
|
||||||
|
loaded first.
|
||||||
|
req.params.renderer: This is the Markdown+ version for the static theme. If a
|
||||||
|
User theme the value will come from the User Theme metadata.
|
||||||
|
*/
|
||||||
|
req.params.renderer = _.upperFirst(req.params.renderer);
|
||||||
|
let currentTheme;
|
||||||
|
const completeStyles = [];
|
||||||
|
const completeSnippets = [];
|
||||||
|
|
||||||
|
while (req.params.id) {
|
||||||
|
//=== User Themes ===//
|
||||||
|
if(!isStaticTheme(req.params.renderer, req.params.id)) {
|
||||||
|
await api.getBrew('share')(req, res, ()=>{})
|
||||||
|
.catch((err)=>{
|
||||||
|
if(err.HBErrorCode == '05')
|
||||||
|
err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' };
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
currentTheme = req.brew;
|
||||||
|
splitTextStyleAndMetadata(currentTheme);
|
||||||
|
|
||||||
|
// If there is anything in the snippets or style members, append them to the appropriate array
|
||||||
|
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
||||||
|
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
|
||||||
|
|
||||||
|
req.params.id = currentTheme.theme;
|
||||||
|
req.params.renderer = currentTheme.renderer;
|
||||||
|
}
|
||||||
|
//=== Static Themes ===//
|
||||||
|
else {
|
||||||
|
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
||||||
|
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
||||||
|
completeSnippets.push(localSnippets);
|
||||||
|
completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`);
|
||||||
|
|
||||||
|
req.params.id = Themes[req.params.renderer][req.params.id].baseTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnObj = {
|
||||||
|
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
||||||
|
styles : completeStyles.reverse(),
|
||||||
|
snippets : completeSnippets.reverse()
|
||||||
|
};
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(200).send(returnObj);
|
||||||
|
},
|
||||||
updateBrew : async (req, res)=>{
|
updateBrew : async (req, res)=>{
|
||||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
// 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 brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
@@ -369,5 +466,6 @@ router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api
|
|||||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||||
|
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ describe('Tests for api', ()=>{
|
|||||||
let saved;
|
let saved;
|
||||||
|
|
||||||
beforeEach(()=>{
|
beforeEach(()=>{
|
||||||
|
jest.resetModules();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
|
||||||
saved = undefined;
|
saved = undefined;
|
||||||
saveFunc = jest.fn(async function() {
|
saveFunc = jest.fn(async function() {
|
||||||
saved = { ...this, _id: '1' };
|
saved = { ...this, _id: '1' };
|
||||||
@@ -45,8 +48,9 @@ describe('Tests for api', ()=>{
|
|||||||
model.mockImplementation((brew)=>modelBrew(brew));
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
res = {
|
res = {
|
||||||
status : jest.fn(()=>res),
|
status : jest.fn(()=>res),
|
||||||
send : jest.fn(()=>{})
|
send : jest.fn(()=>{}),
|
||||||
|
setHeader : jest.fn(()=>{})
|
||||||
};
|
};
|
||||||
|
|
||||||
api = require('./homebrew.api');
|
api = require('./homebrew.api');
|
||||||
@@ -81,10 +85,6 @@ describe('Tests for api', ()=>{
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(()=>{
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getId', ()=>{
|
describe('getId', ()=>{
|
||||||
it('should return only id if google id is not present', ()=>{
|
it('should return only id if google id is not present', ()=>{
|
||||||
const { id, googleId } = api.getId({
|
const { id, googleId } = api.getId({
|
||||||
@@ -581,6 +581,121 @@ brew`);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Theme bundle', ()=>{
|
||||||
|
it('should return Theme Bundle for a User Theme', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
||||||
|
snippets : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Theme Bundle for nested User Themes', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||||
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||||
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : [
|
||||||
|
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||||
|
],
|
||||||
|
snippets : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Theme Bundle for a Static Theme', async ()=>{
|
||||||
|
const req = { params: { renderer: 'V3', id: '5ePHB' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : [
|
||||||
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
||||||
|
],
|
||||||
|
snippets : [
|
||||||
|
'V3_Blank',
|
||||||
|
'V3_5ePHB'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||||
|
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
||||||
|
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
await api.getThemeBundle(req, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
styles : [
|
||||||
|
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
||||||
|
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
||||||
|
`/* From Theme 5eDMG */\n\n@import url("/themes/V3/5eDMG/style.css");`,
|
||||||
|
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
||||||
|
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
||||||
|
],
|
||||||
|
snippets : [
|
||||||
|
'V3_Blank',
|
||||||
|
'V3_5ePHB',
|
||||||
|
'V3_5eDMG'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail for an invalid Theme in the chain', async()=>{
|
||||||
|
const brews = {
|
||||||
|
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
||||||
|
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
await api.getThemeBundle(req, res)
|
||||||
|
.catch((e)=>err = e);
|
||||||
|
|
||||||
|
expect(err).toEqual({
|
||||||
|
HBErrorCode : '09',
|
||||||
|
accessType : 'share',
|
||||||
|
brewId : 'missingTheme',
|
||||||
|
message : 'Theme Not Found',
|
||||||
|
name : 'ThemeLoad Error',
|
||||||
|
status : 404 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('deleteBrew', ()=>{
|
describe('deleteBrew', ()=>{
|
||||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
|
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null, filter=null){
|
||||||
const query = { authors: username, published: true };
|
const query = { authors: username, published: true, ...filter };
|
||||||
if(allowAccess){
|
if(allowAccess){
|
||||||
delete query.published;
|
delete query.published;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
|
const request = require('../client/homebrew/utils/request-middleware.js');
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
const splitTextStyleAndMetadata = (brew)=>{
|
||||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||||
@@ -15,6 +16,11 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
brew.style = brew.text.slice(7, index - 1);
|
brew.style = brew.text.slice(7, index - 1);
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
}
|
}
|
||||||
|
if(brew.text.startsWith('```snippets')) {
|
||||||
|
const index = brew.text.indexOf('```\n\n');
|
||||||
|
brew.snippets = brew.text.slice(11, index - 1);
|
||||||
|
brew.text = brew.text.slice(index + 5);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const printCurrentBrew = ()=>{
|
const printCurrentBrew = ()=>{
|
||||||
@@ -28,7 +34,24 @@ const printCurrentBrew = ()=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchThemeBundle = async (obj, renderer, theme)=>{
|
||||||
|
const res = await request
|
||||||
|
.get(`/api/theme/${renderer}/${theme}`)
|
||||||
|
.catch((err)=>{
|
||||||
|
obj.setState({ error: err });
|
||||||
|
});
|
||||||
|
if(!res) return;
|
||||||
|
|
||||||
|
const themeBundle = res.body;
|
||||||
|
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
|
||||||
|
obj.setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
|
themeBundle : themeBundle
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
splitTextStyleAndMetadata,
|
splitTextStyleAndMetadata,
|
||||||
printCurrentBrew
|
printCurrentBrew,
|
||||||
|
fetchThemeBundle,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name" : "5e PHB",
|
"name" : "5e PHB",
|
||||||
"renderer" : "V3",
|
"renderer" : "V3",
|
||||||
"baseTheme" : false,
|
"baseTheme" : "Blank",
|
||||||
"baseSnippets" : false
|
"baseSnippets" : false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name" : "Journal",
|
"name" : "Journal",
|
||||||
"renderer" : "V3",
|
"renderer" : "V3",
|
||||||
"baseTheme" : false,
|
"baseTheme" : "Blank",
|
||||||
"baseSnippets" : "5ePHB"
|
"baseSnippets" : "5ePHB"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,8 +74,9 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: SolberaImitationRemake; //Tweaked 5e version
|
font-family: SolberaImitationRemake; //Tweaked 5e version
|
||||||
src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2');
|
src: url('../../../fonts/5e/Solbera Imitation Tweak.woff2');
|
||||||
font-weight: normal;
|
font-weight: 100 1000;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cover Page */
|
/* Cover Page */
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"5ePHB": {
|
"5ePHB": {
|
||||||
"name": "5e PHB",
|
"name": "5e PHB",
|
||||||
"renderer": "V3",
|
"renderer": "V3",
|
||||||
"baseTheme": false,
|
"baseTheme": "Blank",
|
||||||
"baseSnippets": false,
|
"baseSnippets": false,
|
||||||
"path": "5ePHB"
|
"path": "5ePHB"
|
||||||
},
|
},
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"Journal": {
|
"Journal": {
|
||||||
"name": "Journal",
|
"name": "Journal",
|
||||||
"renderer": "V3",
|
"renderer": "V3",
|
||||||
"baseTheme": false,
|
"baseTheme": "Blank",
|
||||||
"baseSnippets": "5ePHB",
|
"baseSnippets": "5ePHB",
|
||||||
"path": "Journal"
|
"path": "Journal"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user