0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-09 02:52:38 +00:00

Merge branch 'master' into pr/2745

This commit is contained in:
Trevor Buckner
2023-03-22 14:11:19 -04:00
30 changed files with 16121 additions and 29247 deletions

View File

@@ -48,7 +48,7 @@ jobs:
- image: cimg/node:16.11.0 - image: cimg/node:16.11.0
working_directory: ~/homebrewery working_directory: ~/homebrewery
parallelism: 4 parallelism: 1
steps: steps:
- attach_workspace: - attach_workspace:
@@ -61,15 +61,15 @@ jobs:
- run: - run:
name: Test - Basic name: Test - Basic
command: npm run test:basic command: npm run test:basic
- run:
name: Test - Coverage
command: npm run test:coverage
- run: - run:
name: Test - Mustache Spans name: Test - Mustache Spans
command: npm run test:mustache-span command: npm run test:mustache-span
- run: - run:
name: Test - Routes name: Test - Routes
command: npm run test:route command: npm run test:route
- run:
name: Test - Coverage
command: npm run test:coverage
workflows: workflows:
build_and_test: build_and_test:

View File

@@ -80,6 +80,37 @@ 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 13/03/2023 - v3.7.2
{{taskList
##### Calculuschild
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
}}
### Thursday 09/03/2023 - v3.7.1
{{taskList
##### Lucastucious (new contributor!)
* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making text selectable in PDFs again.
Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
{{note
**NOTE:** If you create your PDF on a computer with an old version of Mac Preview (v10 or older) you may see shadows appear as solid gray.
}}
##### MichielDeMey
* [x] Updated the Google Drive icon
* [x] Backend fix to unit tests failing intermittently
##### Calculuschild
* [x] Fix PDF pixelation on CoverPage text outlines
}}
### Tuesday 28/02/2023 - v3.7.0 ### Tuesday 28/02/2023 - v3.7.0
{{taskList {{taskList
@@ -115,7 +146,6 @@ Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687)
Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674) Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674)
}} }}
### Monday 23/01/2023 - v3.6.0 ### Monday 23/01/2023 - v3.6.0
{{taskList {{taskList
##### calculuschild ##### calculuschild
@@ -141,8 +171,6 @@ Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
* [x] Fix cloned brews inheriting the parent view count * [x] Fix cloned brews inheriting the parent view count
}} }}
\column
### Friday 23/12/2022 - v3.5.0 ### Friday 23/12/2022 - v3.5.0
{{taskList {{taskList

View File

@@ -0,0 +1,129 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
require('./combobox.less');
const Combobox = createClass({
displayName : 'Combobox',
getDefaultProps : function() {
return {
className : '',
trigger : 'hover',
default : '',
placeholder : '',
autoSuggest : {
clearAutoSuggestOnClick : true,
suggestMethod : 'includes',
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
},
};
},
getInitialState : function() {
return {
showDropdown : false,
value : '',
options : [...this.props.options],
inputFocused : false
};
},
componentDidMount : function() {
if(this.props.trigger == 'click')
document.addEventListener('click', this.handleClickOutside);
this.setState({
value : this.props.default
});
},
componentWillUnmount : function() {
if(this.props.trigger == 'click')
document.removeEventListener('click', this.handleClickOutside);
},
handleClickOutside : function(e){
// Close dropdown when clicked outside
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
this.handleDropdown(false);
}
},
handleDropdown : function(show){
this.setState({
showDropdown : show,
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
});
},
handleInput : function(e){
e.persist();
this.setState({
value : e.target.value,
inputFocused : false
}, ()=>{
this.props.onEntry(e);
});
},
handleSelect : function(e){
this.setState({
value : e.currentTarget.getAttribute('data-value')
}, ()=>{this.props.onSelect(this.state.value);});
;
},
renderTextInput : function(){
return (
<div className='dropdown-input item'
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
<input
type='text'
onChange={(e)=>this.handleInput(e)}
value={this.state.value || ''}
placeholder={this.props.placeholder}
onBlur={(e)=>{
if(!e.target.checkValidity()){
this.setState({
value : this.props.default
}, ()=>this.props.onEntry(e));
}
}}
/>
</div>
);
},
renderDropdown : function(dropdownChildren){
if(!this.state.showDropdown) return null;
if(this.props.autoSuggest && !this.state.inputFocused){
const suggestMethod = this.props.autoSuggest.suggestMethod;
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
const filteredArrays = filterOn.map((attr)=>{
const children = dropdownChildren.filter((item)=>{
if(suggestMethod === 'includes'){
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
} else if(suggestMethod === 'startsWith'){
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
}
});
return children;
});
dropdownChildren = _.uniq(filteredArrays.flat(1));
}
return (
<div className='dropdown-options'>
{dropdownChildren}
</div>
);
},
render : function () {
const dropdownChildren = this.state.options.map((child, i)=>{
const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
return clone;
});
return (
<div className={`dropdown-container ${this.props.className}`}
ref='dropdown'
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
{this.renderTextInput()}
{this.renderDropdown(dropdownChildren)}
</div>
);
}
});
module.exports = Combobox;

View File

@@ -0,0 +1,50 @@
.dropdown-container {
position:relative;
input {
width: 100%;
}
.dropdown-options {
position:absolute;
background-color: white;
z-index: 100;
width: 100%;
border: 1px solid gray;
overflow-y: auto;
max-height: 200px;
&::-webkit-scrollbar {
width: 14px;
}
&::-webkit-scrollbar-track {
background: #ffffff;
}
&::-webkit-scrollbar-thumb {
background-color: #949494;
border-radius: 10px;
border: 3px solid #ffffff;
}
.item {
position:relative;
font-size: 11px;
font-family: Open Sans;
padding: 5px;
cursor: default;
margin: 0 3px;
//border-bottom: 1px solid darkgray;
&:hover {
filter: brightness(120%);
background-color: rgb(163, 163, 163);
}
.detail {
width:100%;
text-align: left;
color: rgb(124, 124, 124);
font-style:italic;
font-size: 9px;
}
}
}
}

View File

@@ -27,6 +27,7 @@ const BrewRenderer = createClass({
style : '', style : '',
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB', theme : '5ePHB',
lang : '',
errors : [] errors : []
}; };
}, },
@@ -190,7 +191,6 @@ const BrewRenderer = createClass({
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy'; const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB'; const themePath = this.props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme; const baseThemePath = Themes[rendererPath][themePath].baseTheme;
return ( return (
<React.Fragment> <React.Fragment>
{!this.state.isMounted {!this.state.isMounted
@@ -223,7 +223,7 @@ const BrewRenderer = createClass({
&& &&
<> <>
{this.renderStyle()} {this.renderStyle()}
<div className='pages' ref='pages'> <div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
{this.renderPages()} {this.renderPages()}
</div> </div>
</> </>

View File

@@ -6,6 +6,7 @@ const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require('../../utils/request-middleware.js'); const request = require('../../utils/request-middleware.js');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.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');
@@ -35,7 +36,8 @@ const MetadataEditor = createClass({
authors : [], authors : [],
systems : [], systems : [],
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB' theme : '5ePHB',
lang : 'en'
}, },
onChange : ()=>{}, onChange : ()=>{},
reportError : ()=>{} reportError : ()=>{}
@@ -76,6 +78,7 @@ const MetadataEditor = createClass({
const errMessage = validationErr.map((err)=>{ const errMessage = validationErr.map((err)=>{
return `- ${err}`; return `- ${err}`;
}).join('\n'); }).join('\n');
callIfExists(e.target, 'setCustomValidity', errMessage); callIfExists(e.target, 'setCustomValidity', errMessage);
callIfExists(e.target, 'reportValidity'); callIfExists(e.target, 'reportValidity');
} }
@@ -111,6 +114,11 @@ const MetadataEditor = createClass({
this.props.onChange(this.props.metadata); this.props.onChange(this.props.metadata);
}, },
handleLanguage : function(languageCode){
this.props.metadata.lang = languageCode;
this.props.onChange(this.props.metadata);
},
handleDelete : function(){ handleDelete : function(){
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){ if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return; if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
@@ -224,6 +232,47 @@ const MetadataEditor = createClass({
</div>; </div>;
}, },
renderLanguageDropdown : function(){
const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant'];
const listLanguages = ()=>{
return _.map(langCodes.sort(), (code, index)=>{
const localName = new Intl.DisplayNames([code], { type: 'language' });
const englishName = new Intl.DisplayNames('en', { type: 'language' });
return <div className='item' title={`${englishName.of(code)}`} key={`${index}`} data-value={`${code}`} data-detail={`${localName.of(code)}`}>
{`${code}`}
<div className='detail'>{`${localName.of(code)}`}</div>
</div>;
});
};
const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
return <div className='field language'>
<label>language</label>
<div className='value'>
<Combobox trigger='click'
className='language-dropdown'
default={this.props.metadata.lang || ''}
placeholder='en'
onSelect={(value)=>this.handleLanguage(value)}
onEntry={(e)=>{
e.target.setCustomValidity(''); //Clear the validation popup while typing
debouncedHandleFieldChange('lang', e);
}}
options={listLanguages()}
autoSuggest={{
suggestMethod : 'startsWith',
clearAutoSuggestOnClick : true,
filterOn : ['data-value', 'data-detail', 'title']
}}
>
</Combobox>
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
</div>
</div>;
},
renderRenderOptions : function(){ renderRenderOptions : function(){
if(!global.enable_v3) return; if(!global.enable_v3) return;
@@ -301,6 +350,8 @@ const MetadataEditor = createClass({
</div> </div>
</div> </div>
{this.renderLanguageDropdown()}
{this.renderThemeDropdown()} {this.renderThemeDropdown()}
{this.renderRenderOptions()} {this.renderRenderOptions()}
@@ -315,7 +366,7 @@ const MetadataEditor = createClass({
validators={[(v)=>!this.props.metadata.authors?.includes(v)]} validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true} placeholder='invite author' unique={true}
values={this.props.metadata.invitedAuthors} values={this.props.metadata.invitedAuthors}
notes={['Invited authors are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/> onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
<hr/> <hr/>

View File

@@ -36,11 +36,15 @@
flex: 5 0 200px; flex: 5 0 200px;
gap: 10px; gap: 10px;
} }
.field{ .field{
display : flex; display : flex;
flex-wrap : wrap; flex-wrap : wrap;
width : 100%; width : 100%;
min-width : 200px; min-width : 200px;
position : relative;
&>label{ &>label{
width : 80px; width : 80px;
font-size : 11px; font-size : 11px;
@@ -57,6 +61,9 @@
} }
input[type='text'], textarea { input[type='text'], textarea {
border : 1px solid gray; border : 1px solid gray;
&:focus {
outline: 1px solid #444;
}
} }
&.thumbnail{ &.thumbnail{
height : 1.4em; height : 1.4em;
@@ -88,9 +95,15 @@
} }
} }
&.language .language-dropdown {
max-width : 150px;
z-index : 200;
}
small { small {
font-size : 0.6em; font-size : 0.6em;
font-style : italic; font-style : italic;
line-height : 1.4em;
display : inline-block;
} }
} }
@@ -159,7 +172,7 @@
.navDropdownContainer { .navDropdownContainer {
background-color : white; background-color : white;
position : relative; position : relative;
z-index : 500; z-index : 100;
&.disabled { &.disabled {
font-style :italic; font-style :italic;
font-style : italic; font-style : italic;

View File

@@ -23,9 +23,9 @@ module.exports = {
} }
} }
], ],
language : [ lang : [
(value)=>{ (value)=>{
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null; return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
} }
] ]
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

View File

@@ -0,0 +1,8 @@
<svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg">
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/>
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
</svg>

After

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -47,6 +47,7 @@ const Homebrew = createClass({
editId : null, editId : null,
createdAt : null, createdAt : null,
updatedAt : null, updatedAt : null,
lang : ''
} }
}; };
}, },

View File

@@ -63,7 +63,7 @@ const Account = createClass({
if(global.account){ if(global.account){
return <Nav.dropdown> return <Nav.dropdown>
<Nav.item <Nav.item
className='account' className='account username'
color='orange' color='orange'
icon='fas fa-user' icon='fas fa-user'
> >

View File

@@ -187,4 +187,7 @@
.account.navItem{ .account.navItem{
min-width: 100px; min-width: 100px;
} }
.account.username.navItem{
text-transform: none;
}
} }

View File

@@ -6,7 +6,7 @@ const cx = require('classnames');
const moment = require('moment'); const moment = require('moment');
const request = require('../../../../utils/request-middleware.js'); const request = require('../../../../utils/request-middleware.js');
const googleDriveIcon = require('../../../../googleDrive.png'); const googleDriveIcon = require('../../../../googleDrive.svg');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const BrewItem = createClass({ const BrewItem = createClass({

View File

@@ -94,7 +94,7 @@
} }
} }
.googleDriveIcon { .googleDriveIcon {
height : 20px; height : 18px;
padding : 0px; padding : 0px;
margin : -5px; margin : -5px;
} }

View File

@@ -219,7 +219,7 @@ const ListPage = createClass({
render : function(){ render : function(){
return <div className='listPage sitePage'> return <div className='listPage sitePage'>
//<style>@layer V3_5ePHB, bundle;</style> {/*<style>@layer V3_5ePHB, bundle;</style>*/}
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/> <link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
{this.props.navItems} {this.props.navItems}
{this.renderSortOptions()} {this.renderSortOptions()}

View File

@@ -24,8 +24,7 @@ const Markdown = require('naturalcrit/markdown.js');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const googleDriveActive = require('../../googleDrive.png'); const googleDriveIcon = require('../../googleDrive.svg');
const googleDriveInactive = require('../../googleDriveMono.png');
const SAVE_TIMEOUT = 3000; const SAVE_TIMEOUT = 3000;
@@ -222,10 +221,7 @@ const EditPage = createClass({
renderGoogleDriveIcon : function(){ renderGoogleDriveIcon : function(){
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}> return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
{this.state.saveGoogle <img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
? <img src={googleDriveActive} alt='googleDriveActive'/>
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
}
{this.state.confirmGoogleTransfer && {this.state.confirmGoogleTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}> <div className='errorContainer' onClick={this.closeAlerts}>
@@ -402,7 +398,14 @@ const EditPage = createClass({
reportError={this.errorReported} reportError={this.errorReported}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
/> />
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors} /> <BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
/>
</SplitPane> </SplitPane>
</div> </div>
</div>; </div>;

View File

@@ -18,8 +18,12 @@
position : relative; position : relative;
} }
.googleDriveStorage img{ .googleDriveStorage img{
height : 20px; height : 18px;
padding : 0px; padding : 0px;
margin : -5px; margin : -5px;
&.inactive {
filter: grayscale(1);
}
} }
} }

View File

@@ -61,6 +61,7 @@ const NewPage = createClass({
// brew.description = metaStorage?.description || this.state.brew.description; // brew.description = metaStorage?.description || this.state.brew.description;
brew.renderer = metaStorage?.renderer ?? brew.renderer; brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme; brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang;
this.setState({ this.setState({
brew : brew brew : brew
@@ -70,7 +71,7 @@ const NewPage = createClass({
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);
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme })); localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
}, },
componentWillUnmount : function() { componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
@@ -114,13 +115,16 @@ const NewPage = createClass({
handleMetaChange : function(metadata){ handleMetaChange : function(metadata){
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, ...metadata }, brew : { ...prevState.brew, ...metadata },
})); }), ()=>{
localStorage.setItem(METAKEY, JSON.stringify({ localStorage.setItem(METAKEY, JSON.stringify({
// 'title' : this.state.brew.title, // 'title' : this.state.brew.title,
// 'description' : this.state.brew.description, // 'description' : this.state.brew.description,
'renderer' : this.state.brew.renderer, 'renderer' : this.state.brew.renderer,
'theme' : this.state.brew.theme 'theme' : this.state.brew.theme,
})); 'lang' : this.state.brew.lang
}));
});
;
}, },
save : async function(){ save : async function(){
@@ -211,7 +215,7 @@ const NewPage = createClass({
onMetaChange={this.handleMetaChange} onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer} renderer={this.state.brew.renderer}
/> />
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors}/> <BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} lang={this.state.brew.lang} errors={this.state.htmlErrors}/>
</SplitPane> </SplitPane>
</div> </div>
</div>; </div>;

44843
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.7.0", "version": "3.7.2",
"engines": { "engines": {
"node": "16.11.x" "node": "16.11.x"
}, },
@@ -19,9 +19,9 @@
"lint:dry": "eslint **/*.{js,jsx}", "lint:dry": "eslint **/*.{js,jsx}",
"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", "test": "jest --runInBand",
"test:api-unit": "jest server/*.spec.js --verbose", "test:api-unit": "jest server/*.spec.js --verbose",
"test:coverage": "jest --coverage --silent", "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",
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose", "test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
@@ -37,12 +37,15 @@
"build/*" "build/*"
], ],
"jest": { "jest": {
"testTimeout": 15000, "testTimeout": 30000,
"modulePaths": [ "modulePaths": [
"node_modules", "node_modules",
"shared", "shared",
"server" "server"
], ],
"coveragePathIgnorePatterns": [
"build/*"
],
"coverageThreshold" : { "coverageThreshold" : {
"global" : { "global" : {
"statements" : 25, "statements" : 25,
@@ -68,10 +71,11 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.21.0", "@babel/core": "^7.21.3",
"@babel/plugin-transform-runtime": "^7.21.0", "@babel/plugin-transform-runtime": "^7.21.0",
"@babel/preset-env": "^7.19.4", "@babel/preset-env": "^7.19.4",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"@googleapis/drive": "^5.0.1",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
@@ -81,8 +85,7 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.7",
"fs-extra": "11.1.0", "fs-extra": "11.1.1",
"googleapis": "111.0.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
@@ -91,22 +94,22 @@
"marked-extended-tables": "^1.0.5", "marked-extended-tables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4", "moment": "^2.29.4",
"mongoose": "^6.9.2", "mongoose": "^7.0.2",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"npm": "^8.10.0", "npm": "^9.6.2",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-frame-component": "4.1.3", "react-frame-component": "4.1.3",
"react-router-dom": "6.8.2", "react-router-dom": "6.9.0",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^6.1.0", "superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.35.0", "eslint": "^8.36.0",
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",
"jest": "^29.4.3", "jest": "^29.5.0",
"supertest": "^6.3.3" "supertest": "^6.3.3"
} }
} }

View File

@@ -23,7 +23,7 @@ const splitTextStyleAndMetadata = (brew)=>{
const index = brew.text.indexOf('```\n\n'); const index = brew.text.indexOf('```\n\n');
const metadataSection = brew.text.slice(12, index - 1); const metadataSection = brew.text.slice(12, index - 1);
const metadata = yaml.load(metadataSection); const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme'])); Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
brew.text = brew.text.slice(index + 5); brew.text = brew.text.slice(index + 5);
} }
if(brew.text.startsWith('```css')) { if(brew.text.startsWith('```css')) {
@@ -225,6 +225,7 @@ app.get('/user/:username', async (req, res, next)=>{
'pageCount', 'pageCount',
'description', 'description',
'authors', 'authors',
'lang',
'published', 'published',
'views', 'views',
'shareId', 'shareId',

View File

@@ -15,6 +15,7 @@ const DEFAULT_BREW = {
authors : [], authors : [],
tags : [], tags : [],
systems : [], systems : [],
lang : 'en',
thumbnail : '', thumbnail : '',
views : 0, views : 0,
published : false, published : false,

View File

@@ -27,8 +27,8 @@ const disconnect = async ()=>{
}; };
const connect = async (config)=>{ const connect = async (config)=>{
return await Mongoose.connect(getMongoDBURL(config), return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
{ retryWrites: false }, handleConnectionError); .catch((error)=>handleConnectionError(error));
}; };
module.exports = { module.exports = {

View File

@@ -1,6 +1,6 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
const _ = require('lodash'); const _ = require('lodash');
const { google } = require('googleapis'); const googleDrive = require('@googleapis/drive');
const { nanoid } = require('nanoid'); const { nanoid } = require('nanoid');
const token = require('./token.js'); const token = require('./token.js');
const config = require('./config.js'); const config = require('./config.js');
@@ -14,7 +14,7 @@ if(!config.get('service_account')){
config.get('service_account'); config.get('service_account');
try { try {
serviceAuth = google.auth.fromJSON(keys); serviceAuth = googleDrive.auth.fromJSON(keys);
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive']; serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
} catch (err) { } catch (err) {
console.warn(err); console.warn(err);
@@ -22,7 +22,7 @@ if(!config.get('service_account')){
} }
} }
google.options({ auth: serviceAuth || config.get('google_api_key') }); const defaultAuth = serviceAuth || config.get('google_api_key');
const GoogleActions = { const GoogleActions = {
@@ -33,7 +33,7 @@ const GoogleActions = {
throw (err); throw (err);
} }
const oAuth2Client = new google.auth.OAuth2( const oAuth2Client = new googleDrive.auth.OAuth2(
config.get('google_client_id'), config.get('google_client_id'),
config.get('google_client_secret'), config.get('google_client_secret'),
'/auth/google/redirect' '/auth/google/redirect'
@@ -60,7 +60,7 @@ const GoogleActions = {
}, },
getGoogleFolder : async (auth)=>{ getGoogleFolder : async (auth)=>{
const drive = google.drive({ version: 'v3', auth }); const drive = googleDrive.drive({ version: 'v3', auth });
fileMetadata = { fileMetadata = {
'name' : 'Homebrewery', 'name' : 'Homebrewery',
@@ -97,7 +97,7 @@ const GoogleActions = {
}, },
listGoogleBrews : async (auth)=>{ listGoogleBrews : async (auth)=>{
const drive = google.drive({ version: 'v3', auth }); const drive = googleDrive.drive({ version: 'v3', auth });
const obj = await drive.files.list({ const obj = await drive.files.list({
pageSize : 1000, pageSize : 1000,
@@ -129,14 +129,16 @@ const GoogleActions = {
description : file.description, description : file.description,
views : parseInt(file.properties.views), views : parseInt(file.properties.views),
published : file.properties.published ? file.properties.published == 'true' : false, published : file.properties.published ? file.properties.published == 'true' : false,
systems : [] systems : [],
lang : file.properties.lang,
thumbnail : file.properties.thumbnail
}; };
}); });
return brews; return brews;
}, },
updateGoogleBrew : async (brew)=>{ updateGoogleBrew : async (brew)=>{
const drive = google.drive({ version: 'v3' }); const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
await drive.files.update({ await drive.files.update({
fileId : brew.googleId, fileId : brew.googleId,
@@ -149,7 +151,8 @@ const GoogleActions = {
editId : brew.editId || nanoid(12), editId : brew.editId || nanoid(12),
pageCount : brew.pageCount, pageCount : brew.pageCount,
renderer : brew.renderer || 'legacy', renderer : brew.renderer || 'legacy',
isStubbed : true isStubbed : true,
lang : brew.lang || 'en'
} }
}, },
media : { media : {
@@ -167,7 +170,7 @@ const GoogleActions = {
}, },
newGoogleBrew : async (auth, brew)=>{ newGoogleBrew : async (auth, brew)=>{
const drive = google.drive({ version: 'v3', auth }); const drive = googleDrive.drive({ version: 'v3', auth });
const media = { const media = {
mimeType : 'text/plain', mimeType : 'text/plain',
@@ -187,7 +190,8 @@ const GoogleActions = {
pageCount : brew.pageCount, pageCount : brew.pageCount,
renderer : brew.renderer || 'legacy', renderer : brew.renderer || 'legacy',
isStubbed : true, isStubbed : true,
version : 1 version : 1,
lang : brew.lang || 'en'
} }
}; };
@@ -218,7 +222,7 @@ const GoogleActions = {
}, },
getGoogleBrew : async (id, accessId, accessType)=>{ getGoogleBrew : async (id, accessId, accessType)=>{
const drive = google.drive({ version: 'v3' }); const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
const obj = await drive.files.get({ const obj = await drive.files.get({
fileId : id, fileId : id,
@@ -255,6 +259,7 @@ const GoogleActions = {
description : obj.data.description, description : obj.data.description,
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [], systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
authors : [], authors : [],
lang : obj.data.properties.lang,
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false, published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
trashed : obj.data.trashed, trashed : obj.data.trashed,
@@ -274,7 +279,7 @@ const GoogleActions = {
}, },
deleteGoogleBrew : async (auth, id, accessId)=>{ deleteGoogleBrew : async (auth, id, accessId)=>{
const drive = google.drive({ version: 'v3', auth }); const drive = googleDrive.drive({ version: 'v3', auth });
const obj = await drive.files.get({ const obj = await drive.files.get({
fileId : id, fileId : id,
@@ -300,7 +305,7 @@ const GoogleActions = {
}, },
increaseView : async (id, accessId, accessType, brew)=>{ increaseView : async (id, accessId, accessType, brew)=>{
const drive = google.drive({ version: 'v3' }); const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
await drive.files.update({ await drive.files.update({
fileId : brew.googleId, fileId : brew.googleId,

View File

@@ -62,6 +62,7 @@ describe('Tests for api', ()=>{
description : 'this is a description', description : 'this is a description',
tags : ['something', 'fun'], tags : ['something', 'fun'],
systems : ['D&D 5e'], systems : ['D&D 5e'],
lang : 'en',
renderer : 'v3', renderer : 'v3',
theme : 'phb', theme : 'phb',
published : true, published : true,
@@ -255,6 +256,7 @@ If you believe you should have access to this brew, ask the file owner to invite
pageCount : 1, pageCount : 1,
published : false, published : false,
renderer : 'legacy', renderer : 'legacy',
lang : 'en',
shareId : undefined, shareId : undefined,
systems : [], systems : [],
tags : [], tags : [],
@@ -448,6 +450,7 @@ brew`);
pageCount : 1, pageCount : 1,
published : false, published : false,
renderer : 'V3', renderer : 'V3',
lang : 'en',
shareId : expect.any(String), shareId : expect.any(String),
style : undefined, style : undefined,
systems : [], systems : [],
@@ -506,6 +509,7 @@ brew`);
pageCount : undefined, pageCount : undefined,
published : false, published : false,
renderer : undefined, renderer : undefined,
lang : 'en',
shareId : expect.any(String), shareId : expect.any(String),
googleId : expect.any(String), googleId : expect.any(String),
style : undefined, style : undefined,

View File

@@ -15,6 +15,7 @@ const HomebrewSchema = mongoose.Schema({
description : { type: String, default: '' }, description : { type: String, default: '' },
tags : [String], tags : [String],
systems : [String], systems : [String],
lang : { type: String, default: 'en' },
renderer : { type: String, default: '' }, renderer : { type: String, default: '' },
authors : [String], authors : [String],
invitedAuthors : [String], invitedAuthors : [String],
@@ -39,30 +40,24 @@ HomebrewSchema.statics.increaseView = async function(query) {
return brew; return brew;
}; };
HomebrewSchema.statics.get = function(query, fields=null){ HomebrewSchema.statics.get = async function(query, fields=null){
return new Promise((resolve, reject)=>{ const brew = await Homebrew.findOne(query, fields).orFail()
Homebrew.find(query, fields, null, (err, brews)=>{ .catch((error)=>{throw 'Can not find brew';});
if(err || !brews.length) return reject('Can not find brew'); if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
if(!_.isNil(brews[0].textBin)) { // Uncompress zipped text field unzipped = zlib.inflateRawSync(brew.textBin);
unzipped = zlib.inflateRawSync(brews[0].textBin); brew.text = unzipped.toString();
brews[0].text = unzipped.toString(); }
} return brew;
return resolve(brews[0]);
});
});
}; };
HomebrewSchema.statics.getByUser = function(username, allowAccess=false, fields=null){ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
return new Promise((resolve, reject)=>{ const query = { authors: username, published: true };
const query = { authors: username, published: true }; if(allowAccess){
if(allowAccess){ delete query.published;
delete query.published; }
} const brews = await Homebrew.find(query, fields).lean().exec() //lean() converts results to JSObjects
Homebrew.find(query, fields).lean().exec((err, brews)=>{ //lean() converts results to JSObjects .catch((error)=>{throw 'Can not find brews';});
if(err) return reject('Can not find brew'); return brews;
return resolve(brews);
});
});
}; };
const Homebrew = mongoose.model('Homebrew', HomebrewSchema); const Homebrew = mongoose.model('Homebrew', HomebrewSchema);

View File

@@ -262,6 +262,7 @@ body {
//Full Width //Full Width
hr+hr+blockquote{ hr+hr+blockquote{
.useColumns(0.96); .useColumns(0.96);
column-fill : balance;
} }
//***************************** //*****************************
// * FOOTER // * FOOTER

View File

@@ -253,7 +253,7 @@ body {
border-image-outset : 9px 0px; border-image-outset : 9px 0px;
border-image-width : 11px; border-image-width : 11px;
padding : 0.13cm 0.16cm; padding : 0.13cm 0.16cm;
filter : drop-shadow(1px 4px 6px #888); box-shadow : 1px 4px 14px #888;
.page :where(&) { .page :where(&) {
margin-top : 9px; //Prevent top border getting cut off on colbreak margin-top : 9px; //Prevent top border getting cut off on colbreak
} }
@@ -282,7 +282,7 @@ body {
border-image : @descriptiveBoxImage 12 stretch; border-image : @descriptiveBoxImage 12 stretch;
border-image-outset : 4px; border-image-outset : 4px;
padding : 0.1em; padding : 0.1em;
filter : drop-shadow(0 0 3px #faf7ea); box-shadow : 0 0 6px #faf7ea;
.page :where(&) { .page :where(&) {
margin-top : 4px; //Prevent top border getting cut off on colbreak margin-top : 4px; //Prevent top border getting cut off on colbreak
} }
@@ -397,7 +397,7 @@ body {
border-image-outset : 0px 2px; border-image-outset : 0px 2px;
background-blend-mode : overlay; background-blend-mode : overlay;
background-attachment : fixed; background-attachment : fixed;
filter : drop-shadow(1px 4px 6px #888); box-shadow : 1px 4px 14px #888;
padding : 4px 2px; padding : 4px 2px;
margin-left : -0.16cm; margin-left : -0.16cm;
margin-right : -0.16cm; margin-right : -0.16cm;
@@ -687,11 +687,11 @@ h5 + table{
all: unset; all: unset;
} }
.logo { .logo {
position : absolute; position : absolute;
top : 0.5cm; top : 0.5cm;
left : 0; left : 0;
right : 0; right : 0;
filter : drop-shadow(0 0 0.075cm black); filter :drop-shadow(0 0 0.075cm black);
img { img {
height : 2cm; height : 2cm;
width : 100%; width : 100%;
@@ -706,11 +706,11 @@ h5 + table{
z-index : -1; z-index : -1;
} }
h1 { h1 {
--shadow-x0 : #000 0px 0px 0.1cm; text-shadow: unset;
--shadow-x1 : var(--shadow-x0), var(--shadow-x0), var(--shadow-x0); filter : drop-shadow(0 0 1.5px black) drop-shadow(0 0 0 black)
--shadow-x2 : var(--shadow-x1), var(--shadow-x1), var(--shadow-x1); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
--shadow-x3 : var(--shadow-x2), var(--shadow-x2), var(--shadow-x2); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
text-shadow : var(--shadow-x3), var(--shadow-x3), var(--shadow-x3); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
text-transform : uppercase; text-transform : uppercase;
font-weight : normal; font-weight : normal;
display : block; display : block;
@@ -722,11 +722,10 @@ h5 + table{
line-height : 0.85em; line-height : 0.85em;
} }
h2 { h2 {
--shadow-x0 : #000 0px 0px 2.5px; filter : drop-shadow(0 0 1px black) drop-shadow(0 0 0 black)
--shadow-x1 : var(--shadow-x0), var(--shadow-x0), var(--shadow-x0); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
--shadow-x2 : var(--shadow-x1), var(--shadow-x1), var(--shadow-x1); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
--shadow-x3 : var(--shadow-x2), var(--shadow-x2), var(--shadow-x2); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
text-shadow : var(--shadow-x3), var(--shadow-x3), var(--shadow-x3);
font-family : NodestoCapsCondensed; font-family : NodestoCapsCondensed;
font-weight : normal; font-weight : normal;
font-size : 0.85cm; font-size : 0.85cm;
@@ -746,7 +745,7 @@ h5 + table{
filter : drop-shadow(0 0 3px black); filter : drop-shadow(0 0 3px black);
} }
.banner { .banner {
filter : drop-shadow(2px 2px 2px #000); filter : drop-shadow(2px 2px 2px black);
position : absolute; position : absolute;
left : 0; left : 0;
bottom : 4.2cm; bottom : 4.2cm;
@@ -766,10 +765,10 @@ h5 + table{
padding-top : 0.1cm; padding-top : 0.1cm;
} }
.footnote { .footnote {
--shadow-x0 : #000 0px 0px 0.05cm; filter : drop-shadow(0 0 0.7px black) drop-shadow(0 0 0 black)
--shadow-x1 : var(--shadow-x0), var(--shadow-x0), var(--shadow-x0); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
--shadow-x2 : var(--shadow-x1), var(--shadow-x1), var(--shadow-x1); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black)
text-shadow : var(--shadow-x2), var(--shadow-x2), var(--shadow-x2); drop-shadow(0 0 0 black) drop-shadow(0 0 0 black);
position : absolute; position : absolute;
text-align : center; text-align : center;
color : white; color : white;