0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-09 00:42:47 +00:00

Merge branch 'master' into Language-Attribute

This commit is contained in:
Gazook89
2022-10-20 12:40:49 -05:00
26 changed files with 1544 additions and 3918 deletions

1
.gitignore vendored
View File

@@ -11,3 +11,4 @@ config/docker.*
todo.md todo.md
startDB.bat startDB.bat
startMViewer.bat startMViewer.bat
.vscode

View File

@@ -1,6 +1,7 @@
```css ```css
h5 { h5 {
font-size: .35cm !important; font-size: .35cm !important;
margin-top: 0.3cm;
} }
.page ul ul { .page ul ul {
@@ -14,6 +15,11 @@ h5 {
filter: brightness(1.1) drop-shadow(1px 2px 1px #222); filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
} }
.taskList ul {
margin-bottom: 0px;
margin-top: 0px;
}
.taskList li input[checked] { .taskList li input[checked] {
filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222); filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222);
} }
@@ -39,6 +45,68 @@ 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).
### Friday 19/10/2022 - v3.3.0
{{taskList
##### Calculuschild
* [x] Fix for tables broken by Chrome v106
##### G-Ambatte:
* [x] Fix Table of Contents broken by Chrome v106
Fixes issues [#2437](https://github.com/naturalcrit/homebrewery/issues/2437)
* [x] Show brew thumbnails on user page
Fixes issues [#2331](https://github.com/naturalcrit/homebrewery/issues/2331)
* [x] Allow longer URLs for brew thumbnails
Fixes issues [#2351](https://github.com/naturalcrit/homebrewery/issues/2351)
* [x] Code no longer unfolds when inserting a snippet
Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
* [x] Fix brew settings being lost on first save
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
##### Gazook:
* [x] Several updates to bug reporting and error popups
Fixes issues [#2376](https://github.com/naturalcrit/homebrewery/issues/2376)
* [x] Fixes to userpage search bar
Fixes issues [#1675](https://github.com/naturalcrit/homebrewery/issues/1675), [#2353](https://github.com/naturalcrit/homebrewery/issues/2353)
* [x] Renderer *(legacy / V3)* now shown next to page #
Fixes issues [#1928](https://github.com/naturalcrit/homebrewery/issues/1928)
* [x] Prevent text selection when moving divider bar
Fixes issues [#1632](https://github.com/naturalcrit/homebrewery/issues/1632)
* [x] Tweak Monster Stat Block coloring
Fixes issues [#2123](https://github.com/naturalcrit/homebrewery/issues/2123)
* [x] Added dropdown button to toggle autosave
Fixes issues [#1546](https://github.com/naturalcrit/homebrewery/issues/1546)
}}
### Friday 08/09/2022 - v3.2.2 ### Friday 08/09/2022 - v3.2.2
{{taskList {{taskList

View File

@@ -110,7 +110,12 @@ const BrewRenderer = createClass({
renderPageInfo : function(){ renderPageInfo : function(){
return <div className='pageInfo' ref='main'> return <div className='pageInfo' ref='main'>
{this.state.viewablePageNumber + 1} / {this.state.pages.length} <div>
{this.props.renderer}
</div>
<div>
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div>
</div>; </div>;
}, },

View File

@@ -21,11 +21,17 @@
right : 17px; right : 17px;
bottom : 0; bottom : 0;
z-index : 1000; z-index : 1000;
padding : 8px 10px;
background-color : #333; background-color : #333;
font-size : 10px; font-size : 10px;
font-weight : 800; font-weight : 800;
color : white; color : white;
div {
display: inline-block;
padding : 8px 10px;
&:not(:last-child){
border-right: 1px solid #666;
}
}
} }
.ppr_msg{ .ppr_msg{
position : absolute; position : absolute;

View File

@@ -19,11 +19,6 @@ const DEFAULT_STYLE_TEXT = dedent`
color: black; color: black;
}`; }`;
const splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index);
};
const Editor = createClass({ const Editor = createClass({
displayName : 'Editor', displayName : 'Editor',
@@ -80,19 +75,7 @@ const Editor = createClass({
}, },
handleInject : function(injectText){ handleInject : function(injectText){
let text; this.refs.codeEditor?.injectText(injectText, false);
if(this.isText()) text = this.props.brew.text;
if(this.isStyle()) text = this.props.brew.style ?? DEFAULT_STYLE_TEXT;
const lines = text.split('\n');
const cursorPos = this.refs.codeEditor.getCursorPosition();
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
const injectLines = injectText.split('\n');
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectLines.length, cursorPos.ch + injectLines[injectLines.length - 1].length);
if(this.isText()) this.props.onTextChange(lines.join('\n'));
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
}, },
handleViewChange : function(newView){ handleViewChange : function(newView){

View File

@@ -232,21 +232,25 @@ const MetadataEditor = createClass({
value={this.props.metadata.title} value={this.props.metadata.title}
onChange={(e)=>this.handleFieldChange('title', e)} /> onChange={(e)=>this.handleFieldChange('title', e)} />
</div> </div>
<div className='field description'> <div className='field-group'>
<label>description</label> <div className='field-column'>
<textarea value={this.props.metadata.description} className='value' <div className='field description'>
onChange={(e)=>this.handleFieldChange('description', e)} /> <label>description</label>
</div> <textarea value={this.props.metadata.description} className='value'
<div className='field thumbnail'> onChange={(e)=>this.handleFieldChange('description', e)} />
<label>thumbnail</label> </div>
<input type='text' <div className='field thumbnail'>
value={this.props.metadata.thumbnail} <label>thumbnail</label>
placeholder='my.thumbnail.url' <input type='text'
className='value' value={this.props.metadata.thumbnail}
onChange={(e)=>this.handleFieldChange('thumbnail', e)} /> placeholder='my.thumbnail.url'
<button className='display' onClick={this.toggleThumbnailDisplay}> className='value'
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} /> onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
</button> <button className='display' onClick={this.toggleThumbnailDisplay}>
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
</button>
</div>
</div>
{this.renderThumbnail()} {this.renderThumbnail()}
</div> </div>

View File

@@ -7,23 +7,41 @@
width : 100%; width : 100%;
padding : 25px; padding : 25px;
background-color : #999; background-color : #999;
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
overflow-y : auto;
& > div {
margin-bottom: 10px;
}
.field-group {
display: flex;
width: 100%;
flex-wrap: wrap;
gap: 10px;
}
.field-column {
display: flex;
flex-direction: column;
flex: 5 0 200px;
gap: 10px;
}
.field{ .field{
display : flex; display : flex;
width : 100%; width : 100%;
margin-bottom : 10px; min-width : 200px;
&>label{ &>label{
display : inline-block;
vertical-align : top;
width : 80px; width : 80px;
font-size : 11px; font-size : 11px;
font-weight : 800; font-weight : 800;
line-height : 1.8em; line-height : 1.8em;
text-transform : uppercase; text-transform : uppercase;
flex : 0 0 auto;
} }
&>.value{ &>.value{
flex : 1 1 auto; flex : 1 1 auto;
min-width : 200px; width : 50px;
} }
&.thumbnail{ &.thumbnail{
height : 1.4em; height : 1.4em;
@@ -43,22 +61,32 @@
background-color: #777; background-color: #777;
} }
} }
.thumbnail-preview{ }
position : relative;
width : 80px; &.description {
height : min-content; flex: 1;
border : 2px solid white; textarea.value {
margin-left : 5px; resize : none;
max-height : 115px; height : auto;
font-family : 'Open Sans', sans-serif;
font-size : 0.8em;
} }
} }
} }
.description.field textarea.value{
resize : none;
height : 5em; .thumbnail-preview {
font-family : 'Open Sans', sans-serif; position: relative;
font-size : 0.8em; justify-self: center;
width: 80px;
height: min-content;
flex: 1 1;
max-height: 115px;
aspect-ratio: 1 / 1;
object-fit: contain;
background-color: #AAA;
} }
.systems.field .value{ .systems.field .value{
label{ label{
vertical-align : middle; vertical-align : middle;

View File

@@ -11,7 +11,10 @@
position : relative; position : relative;
height : calc(~"100% - 29px"); //Navbar height height : calc(~"100% - 29px"); //Navbar height
flex : auto; flex : auto;
overflow-y : hidden;
}
&.listPage .content {
overflow-y : scroll; overflow-y : scroll;
} }
} }
} }

View File

@@ -70,7 +70,7 @@ const Account = createClass({
{global.account.username} {global.account.username}
</Nav.item> </Nav.item>
<Nav.item <Nav.item
href={`/user/${global.account.username}`} href={`/user/${encodeURI(global.account.username)}`}
color='yellow' color='yellow'
icon='fas fa-beer' icon='fas fa-beer'
> >

View File

@@ -55,6 +55,18 @@
text-align : center; text-align : center;
text-transform : initial; text-transform : initial;
} }
.save-menu {
.dropdown {
z-index: 1000;
}
.navItem i.fa-power-off {
color : red;
&.active {
color : rgb(0, 182, 52);
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765))
}
}
}
.patreon.navItem{ .patreon.navItem{
border-left : 1px solid #666; border-left : 1px solid #666;
border-right : 1px solid #666; border-right : 1px solid #666;
@@ -78,6 +90,8 @@
width : 100%; width : 100%;
overflow : hidden auto; overflow : hidden auto;
max-height : ~"calc(100vh - 28px)"; max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333;
scrollbar-width : thin;
h4{ h4{
display : block; display : block;
box-sizing : border-box; box-sizing : border-box;

View File

@@ -96,11 +96,15 @@ const BrewItem = createClass({
render : function(){ render : function(){
const brew = this.props.brew; const brew = this.props.brew;
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
brew.tags = brew.tags?.filter(tag => tag); //remove tags that are empty strings brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
} }
const dateFormatString = 'YYYY-MM-DD HH:mm:ss'; const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
return <div className='brewItem'> return <div className='brewItem'>
{brew.thumbnail &&
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
</div>
}
<div className='text'> <div className='text'>
<h2>{brew.title}</h2> <h2>{brew.title}</h2>
<p className='description'>{brew.description}</p> <p className='description'>{brew.description}</p>
@@ -112,7 +116,7 @@ const BrewItem = createClass({
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}> <div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
<i className='fas fa-tags'/> <i className='fas fa-tags'/>
{brew.tags.map((tag, idx)=>{ {brew.tags.map((tag, idx)=>{
let matches = tag.match(/^(?:([^:]+):)?([^:]+)$/); const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
return <span className={matches[1]}>{matches[2]}</span>; return <span className={matches[1]}>{matches[2]}</span>;
})} })}
</div> </div>

View File

@@ -10,7 +10,7 @@
min-height : 105px; min-height : 105px;
margin-right : 15px; margin-right : 15px;
margin-bottom : 15px; margin-bottom : 15px;
padding : 5px 15px 2px 8px; padding : 5px 15px 2px 6px;
padding-right : 15px; padding-right : 15px;
border : 1px solid #c9ad6a; border : 1px solid #c9ad6a;
border-radius : 5px; border-radius : 5px;
@@ -19,6 +19,20 @@
break-inside : avoid; break-inside : avoid;
box-shadow : 0px 4px 5px 0px #333; box-shadow : 0px 4px 5px 0px #333;
background-color : #cab2802e; background-color : #cab2802e;
.thumbnail {
position: absolute;
width: 150px;
height: 100%;
top: 0;
right: 0;
z-index: -1;
background-size: contain;
background-repeat: no-repeat;
background-position: right top;
mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
-webkit-mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
opacity: 50%;
}
.text { .text {
min-height : 54px; min-height : 54px;
h4{ h4{

View File

@@ -114,15 +114,17 @@ const ListPage = createClass({
}, },
renderSortOption : function(sortTitle, sortValue){ renderSortOption : function(sortTitle, sortValue){
return <td> return <div className={`sort-option ${(this.state.sortType == sortValue ? 'active' : '')}`}>
<button <button
value={`${sortValue}`} value={`${sortValue}`}
onClick={this.handleSortOptionChange} onClick={this.state.sortType == sortValue ? this.handleSortDirChange : this.handleSortOptionChange}
className={`${(this.state.sortType == sortValue ? 'active' : '')}`} >
> {`${sortTitle}`}
{`${sortTitle}`} </button>
</button> {this.state.sortType == sortValue &&
</td>; <i className={`sortDir fas ${this.state.sortDir == 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`}></i>
}
</div>;
}, },
handleFilterTextChange : function(e){ handleFilterTextChange : function(e){
@@ -150,7 +152,7 @@ const ListPage = createClass({
}, },
renderFilterOption : function(){ renderFilterOption : function(){
return <td> return <div className='filter-option'>
<label> <label>
<i className='fas fa-search'></i> <i className='fas fa-search'></i>
<input <input
@@ -160,37 +162,22 @@ const ListPage = createClass({
value={this.state.filterString} value={this.state.filterString}
/> />
</label> </label>
</td>; </div>;
}, },
renderSortOptions : function(){ renderSortOptions : function(){
return <div className='sort-container'> return <div className='sort-container'>
<table> <h6>Sort by :</h6>
<tbody> {this.renderSortOption('Title', 'alpha')}
<tr> {this.renderSortOption('Created Date', 'created')}
<td> {this.renderSortOption('Updated Date', 'updated')}
<h6>Sort by :</h6> {this.renderSortOption('Views', 'views')}
</td> {/* {this.renderSortOption('Latest', 'latest')} */}
{this.renderSortOption('Title', 'alpha')}
{this.renderSortOption('Created Date', 'created')} {this.renderFilterOption()}
{this.renderSortOption('Updated Date', 'updated')}
{this.renderSortOption('Views', 'views')}
{/* {this.renderSortOption('Latest', 'latest')} */}
<td>
<h6>Direction :</h6>
</td>
<td>
<button
onClick={this.handleSortDirChange}
className='sortDir'
>
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
</button>
</td>
{this.renderFilterOption()}
</tr>
</tbody>
</table>
</div>; </div>;
}, },
@@ -233,10 +220,10 @@ const ListPage = createClass({
return <div className='listPage sitePage'> return <div className='listPage sitePage'>
<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()}
<div className='content V3'> <div className='content V3'>
<div className='phb page'> <div className='phb page'>
{this.renderSortOptions()}
{this.renderBrewCollection(this.state.brewCollection)} {this.renderBrewCollection(this.state.brewCollection)}
</div> </div>
</div> </div>

View File

@@ -13,7 +13,6 @@
} }
.listPage{ .listPage{
.content{ .content{
overflow-y : overlay;
.phb{ .phb{
.noColumns(); .noColumns();
height : auto; height : auto;
@@ -32,68 +31,74 @@
} }
.sort-container{ .sort-container{
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
position : fixed; position : sticky;
top : 35px; top : 0;
left : calc(50vw - 400px); left : 0;
border : 2px solid #58180D; width : 100%;
width : 800px; height : 30px;
background-color : #EEE5CE; background-color : #555;
padding : 2px; border-top : 1px solid #666;
border-bottom : 1px solid #666;
color : white;
text-align : center; text-align : center;
z-index : 15; z-index : 500;
display : flex;
justify-content : center;
align-items : baseline;
column-gap : 15px;
row-gap : 5px;
flex-wrap : wrap;
h6{ h6{
text-transform : uppercase; text-transform : uppercase;
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
font-size : 11px; font-size : 11px;
font-weight : bold; font-weight : bold;
color : #58180D;
} }
table{ .sort-option {
margin : 0px; display: flex;
vertical-align : middle; align-items: center;
tbody tr{ padding: 0 8px;
background-color: transparent !important; color: #ccc;
i{ height: 100%;
padding-right : 5px
} &:hover{
button{ background-color : #444;
background-color : transparent; }
color : #58180D;
font-family : 'Open Sans', sans-serif; &.active {
font-size : 11px; font-weight: bold;
text-transform : uppercase; color: #ddd;
font-weight : normal; background-color: #333;
&.active{
font-weight : bold; button {
border : 2px solid #58180D; color: white;
} font-weight: 800;
&.sortDir{ height: 100%;
width : 75px; & + .sortDir {
} padding-left: 5px;
} }
} }
} }
}
h1 {
cursor: pointer;
&.active {
color: #58180D;
} }
&.inactive { .filter-option {
color: #707070; margin-left: 20px;
background-color : transparent !important;
} font-size : 11px;
&.active::before, &.inactive::before { i{
font-family: 'Font Awesome 5 Free'; padding-right : 5px;
font-weight: 900; }
font-size: 0.6cm;
padding-right: 0.5em;
}
&.active::before {
content: '\f107';
}
&.inactive::before {
content: '\f105';
} }
button{
background-color : transparent;
font-family : 'Open Sans', sans-serif;
text-transform : uppercase;
font-weight : normal;
font-size : 11px;
color : #ccc;
padding : 0;
}
} }
} }

View File

@@ -62,7 +62,10 @@ const EditPage = createClass({
confirmGoogleTransfer : false, confirmGoogleTransfer : false,
errors : null, errors : null,
htmlErrors : Markdown.validate(this.props.brew.text), htmlErrors : Markdown.validate(this.props.brew.text),
url : '' url : '',
autoSave : true,
autoSaveWarning : false,
unsavedTime : new Date()
}; };
}, },
savedBrew : null, savedBrew : null,
@@ -72,9 +75,17 @@ const EditPage = createClass({
url : window.location.href url : window.location.href
}); });
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
this.trySave(); this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) }, ()=>{
if(this.state.autoSave){
this.trySave();
} else {
this.setState({ autoSaveWarning: true });
}
});
window.onbeforeunload = ()=>{ window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){ if(this.state.isSaving || this.state.isPending){
return 'You have unsaved changes!'; return 'You have unsaved changes!';
@@ -117,14 +128,14 @@ const EditPage = createClass({
brew : { ...prevState.brew, text: text }, brew : { ...prevState.brew, text: text },
isPending : true, isPending : true,
htmlErrors : htmlErrors htmlErrors : htmlErrors
}), ()=>this.trySave()); }), ()=>{if(this.state.autoSave) this.trySave();});
}, },
handleStyleChange : function(style){ handleStyleChange : function(style){
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { ...prevState.brew, style: style }, brew : { ...prevState.brew, style: style },
isPending : true isPending : true
}), ()=>this.trySave()); }), ()=>{if(this.state.autoSave) this.trySave();});
}, },
handleMetaChange : function(metadata){ handleMetaChange : function(metadata){
@@ -134,7 +145,7 @@ const EditPage = createClass({
...metadata ...metadata
}, },
isPending : true, isPending : true,
}), ()=>this.trySave()); }), ()=>{if(this.state.autoSave) this.trySave();});
}, },
@@ -221,8 +232,9 @@ const EditPage = createClass({
editId : this.savedBrew.editId, editId : this.savedBrew.editId,
shareId : this.savedBrew.shareId shareId : this.savedBrew.shareId
}, },
isPending : false, isPending : false,
isSaving : false, isSaving : false,
unsavedTime : new Date()
})); }));
}, },
@@ -329,17 +341,55 @@ const EditPage = createClass({
</Nav.item>; </Nav.item>;
} }
if(this.state.autoSaveWarning && this.hasChanges()){
this.setAutosaveWarning();
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>
{text}
</div>
</Nav.item>;
}
if(this.state.isSaving){ if(this.state.isSaving){
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>; return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
} }
if(this.state.isPending && this.hasChanges()){ if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>; return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
} }
if(!this.state.isPending && !this.state.isSaving && this.state.autoSave){
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
}
if(!this.state.isPending && !this.state.isSaving){ if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item>; return <Nav.item className='save saved'>saved.</Nav.item>;
} }
}, },
handleAutoSave : function(){
if(this.warningTimer) clearTimeout(this.warningTimer);
this.setState((prevState)=>({
autoSave : !prevState.autoSave,
autoSaveWarning : prevState.autoSave
}), ()=>{
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
});
},
setAutosaveWarning : function(){
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
this.warningTimer;
},
renderAutoSaveButton : function(){
return <Nav.item onClick={this.handleAutoSave}>
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>;
},
processShareId : function() { processShareId : function() {
return this.state.brew.googleId && !this.state.brew.stubbed ? return this.state.brew.googleId && !this.state.brew.stubbed ?
this.state.brew.googleId + this.state.brew.shareId : this.state.brew.googleId + this.state.brew.shareId :
@@ -378,7 +428,10 @@ const EditPage = createClass({
<Nav.section> <Nav.section>
{this.renderGoogleDriveIcon()} {this.renderGoogleDriveIcon()}
{this.renderSaveButton()} <Nav.dropdown className='save-menu'>
{this.renderSaveButton()}
{this.renderAutoSaveButton()}
</Nav.dropdown>
<NewBrew /> <NewBrew />
<HelpNavItem/> <HelpNavItem/>
<Nav.dropdown> <Nav.dropdown>

View File

@@ -32,7 +32,7 @@
position : absolute; position : absolute;
top : 100%; top : 100%;
left : 50%; left : 50%;
z-index : 100000; z-index : 500;
width : 140px; width : 140px;
padding : 3px; padding : 3px;
color : white; color : white;

View File

@@ -38,9 +38,7 @@ const HomePage = createClass({
}, },
handleSave : function(){ handleSave : function(){
request.post('/api') request.post('/api')
.send({ .send(this.state.brew)
text : this.state.brew.text
})
.end((err, res)=>{ .end((err, res)=>{
if(err) return; if(err) return;
const brew = res.body; const brew = res.body;

4790
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.2.2", "version": "3.3.0",
"engines": { "engines": {
"node": "16.11.x" "node": "16.11.x"
}, },
@@ -51,44 +51,44 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.19.1", "@babel/core": "^7.19.3",
"@babel/plugin-transform-runtime": "^7.19.1", "@babel/plugin-transform-runtime": "^7.19.1",
"@babel/preset-env": "^7.19.1", "@babel/preset-env": "^7.19.4",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"body-parser": "^1.20.0", "body-parser": "^1.20.1",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.1", "dedent-tabs": "^0.10.1",
"express": "^4.18.1", "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": "10.1.0", "fs-extra": "10.1.0",
"googleapis": "107.0.0", "googleapis": "108.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",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "4.1.0", "marked": "4.1.1",
"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.6.1", "mongoose": "^6.6.5",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"react": "^16.14.0", "react": "^16.14.0",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"react-frame-component": "4.1.3", "react-frame-component": "4.1.3",
"react-router-dom": "6.3.0", "react-router-dom": "6.4.2",
"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.23.1", "eslint": "^8.25.0",
"eslint-plugin-react": "^7.31.8", "eslint-plugin-react": "^7.31.10",
"jest": "^29.0.3", "jest": "^29.2.1",
"supertest": "^6.2.4" "supertest": "^6.3.0"
} }
} }

View File

@@ -181,6 +181,7 @@ app.get('/user/:username', async (req, res, next)=>{
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'lastViewed', 'lastViewed',
'thumbnail',
'tags' 'tags'
]; ];

View File

@@ -147,8 +147,7 @@ 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
thumbnail : brew.thumbnail
} }
}, },
media : { media : {
@@ -186,8 +185,7 @@ const GoogleActions = {
pageCount : brew.pageCount, pageCount : brew.pageCount,
renderer : brew.renderer || 'legacy', renderer : brew.renderer || 'legacy',
isStubbed : true, isStubbed : true,
version : 1, version : 1
thumbnail : brew.thumbnail || ''
} }
}; };
@@ -267,7 +265,6 @@ const GoogleActions = {
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
version : parseInt(obj.data.properties.version) || 0, version : parseInt(obj.data.properties.version) || 0,
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy', renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
thumbnail : obj.data.properties.thumbnail || '',
googleId : id googleId : id
}; };

View File

@@ -108,7 +108,7 @@ const excludePropsFromUpdate = (brew)=>{
const excludeGoogleProps = (brew)=>{ const excludeGoogleProps = (brew)=>{
const modified = _.clone(brew); const modified = _.clone(brew);
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views']; const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
for (const prop of propsToExclude) { for (const prop of propsToExclude) {
delete modified[prop]; delete modified[prop];
} }

View File

@@ -229,6 +229,15 @@ const CodeEditor = createClass({
this.codeMirror.replaceSelection('\n\\page\n\n', 'end'); this.codeMirror.replaceSelection('\n\\page\n\n', 'end');
}, },
injectText : function(injectText, overwrite=true) {
const cm = this.codeMirror;
if(!overwrite) {
cm.setCursor(cm.getCursor('from'));
}
cm.replaceSelection(injectText, 'end');
cm.focus();
},
makeUnderline : function() { makeUnderline : function() {
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>'; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around'); this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
@@ -355,12 +364,20 @@ const CodeEditor = createClass({
let text = ''; let text = '';
let currentLine = from.line; let currentLine = from.line;
const maxLength = 50; const maxLength = 50;
let foldPreviewText = '';
while (currentLine <= to.line && text.length <= maxLength) { while (currentLine <= to.line && text.length <= maxLength) {
text += this.codeMirror.getLine(currentLine); const currentText = this.codeMirror.getLine(currentLine);
if(currentLine < to.line) currentLine++;
text += ' '; if(currentText[0] == '#'){
currentLine += 1; foldPreviewText = currentText;
break;
}
if(!foldPreviewText && currentText != '\n') {
foldPreviewText = currentText;
}
} }
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
text = text.trim(); text = text.trim();
if(text.length > maxLength) if(text.length > maxLength)

View File

@@ -13,7 +13,8 @@
font-family: inherit; font-family: inherit;
text-shadow: none; text-shadow: none;
font-weight: 600; font-weight: 600;
} color: grey;
}
.sourceMoveFlash .CodeMirror-line{ .sourceMoveFlash .CodeMirror-line{
animation-name: sourceMoveAnimation; animation-name: sourceMoveAnimation;

View File

@@ -69,7 +69,8 @@ const SplitPane = createClass({
this.setState({ isDragging: false }); this.setState({ isDragging: false });
}, },
handleDown : function(){ handleDown : function(e){
e.preventDefault();
this.setState({ isDragging: true }); this.setState({ isDragging: true });
//this.unFocus() //this.unFocus()
}, },

View File

@@ -8,7 +8,7 @@
--HB_Color_HeaderUnderline : #C0AD6A; // Gold --HB_Color_HeaderUnderline : #C0AD6A; // Gold
--HB_Color_HorizontalRule : #9C2B1B; // Maroon --HB_Color_HorizontalRule : #9C2B1B; // Maroon
--HB_Color_HeaderText : #58180D; // Dark Maroon --HB_Color_HeaderText : #58180D; // Dark Maroon
--HB_Color_MonsterStatBackground : #EEDBAB; // Light orange parchment --HB_Color_MonsterStatBackground : #F2E5B5; // Light orange parchment
--HB_Color_CaptionText : #766649; // Brown --HB_Color_CaptionText : #766649; // Brown
--HB_Color_WatercolorStain : #BBAD82; // Light brown --HB_Color_WatercolorStain : #BBAD82; // Light brown
--HB_Color_Footnotes : #C9AD6A; // Gold --HB_Color_Footnotes : #C9AD6A; // Gold
@@ -213,7 +213,9 @@ body {
tbody{ tbody{
tr{ tr{
td{ td{
padding : 0.14em 0.4em; //padding : 0.14em 0.4em;
padding : 0px 5px; // Both of these are temporary, just to force
height : 16px; // PDF to render at same height until Chrome 108
} }
&:nth-child(odd){ &:nth-child(odd){
background-color : var(--HB_Color_Accent); background-color : var(--HB_Color_Accent);
@@ -657,72 +659,78 @@ body {
//***************************** //*****************************
// * TABLE OF CONTENTS // * TABLE OF CONTENTS
// *****************************/ // *****************************/
.page .toc{ .page {
&:has(.toc):after {
display: none;
}
.toc {
-webkit-column-break-inside : avoid; -webkit-column-break-inside : avoid;
page-break-inside : avoid; page-break-inside : avoid;
break-inside : avoid; break-inside : avoid;
h1 { h1 {
text-align : center; text-align : center;
margin-bottom : 0.3cm; margin-bottom : 0.3cm;
}
a{
display : inline;
color : inherit;
text-decoration : none;
&:hover{
text-decoration : underline;
} }
} a{
h4 { display : inline;
margin-top : 0.2cm; color : inherit;
line-height : 0.4cm; text-decoration : none;
& + ul li { &:hover{
line-height: 1.2em; text-decoration : underline;
}
} }
} h4 {
ul{ margin-top : 0.2cm;
padding-left : 0; line-height : 0.4cm;
list-style-type : none; & + ul li {
li + li h3 { line-height: 1.2em;
margin-top : 0.26cm; }
line-height : 1em
} }
h3 span:first-child::after { ul{
border : none; padding-left : 0;
} list-style-type : none;
span { margin-top : 0;
display : table-cell; a {
&:first-child { width : 100%;
position : relative; display : flex;
overflow : hidden; flex-flow : row nowrap;
&::after { justify-content : space-between;
}
li + li h3 {
margin-top : 0.26cm;
line-height : 1em
}
h3 span:first-child::after {
border : none;
}
span {
display : contents;
&:first-child::after {
content : ""; content : "";
position : absolute;
bottom : 0.08cm; bottom : 0.08cm;
margin-left : 0.06cm; /* Spacing before dot leaders */ flex : 1;
width : 100%; margin-left : 0.08cm; /* Spacing before dot leaders */
margin-right : 0.16cm;
border-bottom : 0.05cm dotted #000; border-bottom : 0.05cm dotted #000;
margin-bottom : 0.08cm;
}
&:last-child {
display : inline-block;
align-self : flex-end;
font-family : "BookInsanityRemake";
font-size : 0.34cm;
font-weight : normal;
color : #000;
} }
} }
&:last-child { ul { /*List indent*/
font-family : BookInsanityRemake; margin-left : 1em;
font-size : 0.34cm;
font-weight : normal;
color : black;
text-align : right;
vertical-align : bottom; /* Keep page number bottom-aligned */
width : 1%;
padding-left : 0.06cm; /* Spacing after dot leaders */
/*white-space : nowrap; /* Uncomment if needed */
} }
} }
ul { /*List indent*/ &.wide{
margin-left : 1em; .useColumns(0.96, @fillMode: balance);
} }
} }
&.wide{
.useColumns(0.96, @fillMode: balance);
}
} }
//***************************** //*****************************