mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 14:12:40 +00:00
Merge branch 'master' into Language-Attribute
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ config/docker.*
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
.vscode
|
||||
|
||||
68
changelog.md
68
changelog.md
@@ -1,6 +1,7 @@
|
||||
```css
|
||||
h5 {
|
||||
font-size: .35cm !important;
|
||||
margin-top: 0.3cm;
|
||||
}
|
||||
|
||||
.page ul ul {
|
||||
@@ -14,6 +15,11 @@ h5 {
|
||||
filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||
}
|
||||
|
||||
.taskList ul {
|
||||
margin-bottom: 0px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.taskList li input[checked] {
|
||||
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
|
||||
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
|
||||
{{taskList
|
||||
|
||||
|
||||
@@ -110,7 +110,12 @@ const BrewRenderer = createClass({
|
||||
|
||||
renderPageInfo : function(){
|
||||
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>;
|
||||
},
|
||||
|
||||
|
||||
@@ -21,11 +21,17 @@
|
||||
right : 17px;
|
||||
bottom : 0;
|
||||
z-index : 1000;
|
||||
padding : 8px 10px;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
div {
|
||||
display: inline-block;
|
||||
padding : 8px 10px;
|
||||
&:not(:last-child){
|
||||
border-right: 1px solid #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ppr_msg{
|
||||
position : absolute;
|
||||
|
||||
@@ -19,11 +19,6 @@ const DEFAULT_STYLE_TEXT = dedent`
|
||||
color: black;
|
||||
}`;
|
||||
|
||||
const splice = function(str, index, inject){
|
||||
return str.slice(0, index) + inject + str.slice(index);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Editor = createClass({
|
||||
displayName : 'Editor',
|
||||
@@ -80,19 +75,7 @@ const Editor = createClass({
|
||||
},
|
||||
|
||||
handleInject : function(injectText){
|
||||
let text;
|
||||
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'));
|
||||
this.refs.codeEditor?.injectText(injectText, false);
|
||||
},
|
||||
|
||||
handleViewChange : function(newView){
|
||||
|
||||
@@ -232,21 +232,25 @@ const MetadataEditor = createClass({
|
||||
value={this.props.metadata.title}
|
||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||
</div>
|
||||
<div className='field description'>
|
||||
<label>description</label>
|
||||
<textarea value={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text'
|
||||
value={this.props.metadata.thumbnail}
|
||||
placeholder='my.thumbnail.url'
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
<div className='field-group'>
|
||||
<div className='field-column'>
|
||||
<div className='field description'>
|
||||
<label>description</label>
|
||||
<textarea value={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text'
|
||||
value={this.props.metadata.thumbnail}
|
||||
placeholder='my.thumbnail.url'
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderThumbnail()}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,23 +7,41 @@
|
||||
width : 100%;
|
||||
padding : 25px;
|
||||
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{
|
||||
display : flex;
|
||||
width : 100%;
|
||||
margin-bottom : 10px;
|
||||
min-width : 200px;
|
||||
&>label{
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
width : 80px;
|
||||
font-size : 11px;
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
text-transform : uppercase;
|
||||
flex : 0 0 auto;
|
||||
}
|
||||
&>.value{
|
||||
flex : 1 1 auto;
|
||||
min-width : 200px;
|
||||
width : 50px;
|
||||
}
|
||||
&.thumbnail{
|
||||
height : 1.4em;
|
||||
@@ -43,22 +61,32 @@
|
||||
background-color: #777;
|
||||
}
|
||||
}
|
||||
.thumbnail-preview{
|
||||
position : relative;
|
||||
width : 80px;
|
||||
height : min-content;
|
||||
border : 2px solid white;
|
||||
margin-left : 5px;
|
||||
max-height : 115px;
|
||||
}
|
||||
|
||||
&.description {
|
||||
flex: 1;
|
||||
textarea.value {
|
||||
resize : none;
|
||||
height : auto;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.description.field textarea.value{
|
||||
resize : none;
|
||||
height : 5em;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.8em;
|
||||
|
||||
|
||||
.thumbnail-preview {
|
||||
position: relative;
|
||||
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{
|
||||
label{
|
||||
vertical-align : middle;
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
overflow-y : hidden;
|
||||
}
|
||||
&.listPage .content {
|
||||
overflow-y : scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ const Account = createClass({
|
||||
{global.account.username}
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
href={`/user/${global.account.username}`}
|
||||
href={`/user/${encodeURI(global.account.username)}`}
|
||||
color='yellow'
|
||||
icon='fas fa-beer'
|
||||
>
|
||||
|
||||
@@ -55,6 +55,18 @@
|
||||
text-align : center;
|
||||
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{
|
||||
border-left : 1px solid #666;
|
||||
border-right : 1px solid #666;
|
||||
@@ -78,6 +90,8 @@
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
h4{
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
|
||||
@@ -96,11 +96,15 @@ const BrewItem = createClass({
|
||||
render : function(){
|
||||
const brew = this.props.brew;
|
||||
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';
|
||||
|
||||
return <div className='brewItem'>
|
||||
{brew.thumbnail &&
|
||||
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
|
||||
</div>
|
||||
}
|
||||
<div className='text'>
|
||||
<h2>{brew.title}</h2>
|
||||
<p className='description'>{brew.description}</p>
|
||||
@@ -112,7 +116,7 @@ const BrewItem = createClass({
|
||||
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
|
||||
<i className='fas fa-tags'/>
|
||||
{brew.tags.map((tag, idx)=>{
|
||||
let matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return <span className={matches[1]}>{matches[2]}</span>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
min-height : 105px;
|
||||
margin-right : 15px;
|
||||
margin-bottom : 15px;
|
||||
padding : 5px 15px 2px 8px;
|
||||
padding : 5px 15px 2px 6px;
|
||||
padding-right : 15px;
|
||||
border : 1px solid #c9ad6a;
|
||||
border-radius : 5px;
|
||||
@@ -19,6 +19,20 @@
|
||||
break-inside : avoid;
|
||||
box-shadow : 0px 4px 5px 0px #333;
|
||||
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 {
|
||||
min-height : 54px;
|
||||
h4{
|
||||
|
||||
@@ -114,15 +114,17 @@ const ListPage = createClass({
|
||||
},
|
||||
|
||||
renderSortOption : function(sortTitle, sortValue){
|
||||
return <td>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.handleSortOptionChange}
|
||||
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
</td>;
|
||||
return <div className={`sort-option ${(this.state.sortType == sortValue ? 'active' : '')}`}>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.state.sortType == sortValue ? this.handleSortDirChange : this.handleSortOptionChange}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
{this.state.sortType == sortValue &&
|
||||
<i className={`sortDir fas ${this.state.sortDir == 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`}></i>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
|
||||
handleFilterTextChange : function(e){
|
||||
@@ -150,7 +152,7 @@ const ListPage = createClass({
|
||||
},
|
||||
|
||||
renderFilterOption : function(){
|
||||
return <td>
|
||||
return <div className='filter-option'>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
@@ -160,37 +162,22 @@ const ListPage = createClass({
|
||||
value={this.state.filterString}
|
||||
/>
|
||||
</label>
|
||||
</td>;
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>Sort by :</h6>
|
||||
</td>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{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>
|
||||
<h6>Sort by :</h6>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
|
||||
{this.renderFilterOption()}
|
||||
|
||||
|
||||
|
||||
</div>;
|
||||
},
|
||||
|
||||
@@ -233,10 +220,10 @@ const ListPage = createClass({
|
||||
return <div className='listPage sitePage'>
|
||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
{this.renderSortOptions()}
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='phb page'>
|
||||
{this.renderSortOptions()}
|
||||
{this.renderBrewCollection(this.state.brewCollection)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
}
|
||||
.listPage{
|
||||
.content{
|
||||
overflow-y : overlay;
|
||||
.phb{
|
||||
.noColumns();
|
||||
height : auto;
|
||||
@@ -32,68 +31,74 @@
|
||||
}
|
||||
.sort-container{
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
position : fixed;
|
||||
top : 35px;
|
||||
left : calc(50vw - 400px);
|
||||
border : 2px solid #58180D;
|
||||
width : 800px;
|
||||
background-color : #EEE5CE;
|
||||
padding : 2px;
|
||||
position : sticky;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 30px;
|
||||
background-color : #555;
|
||||
border-top : 1px solid #666;
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
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{
|
||||
text-transform : uppercase;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : bold;
|
||||
color : #58180D;
|
||||
}
|
||||
table{
|
||||
margin : 0px;
|
||||
vertical-align : middle;
|
||||
tbody tr{
|
||||
background-color: transparent !important;
|
||||
i{
|
||||
padding-right : 5px
|
||||
}
|
||||
button{
|
||||
background-color : transparent;
|
||||
color : #58180D;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
text-transform : uppercase;
|
||||
font-weight : normal;
|
||||
&.active{
|
||||
font-weight : bold;
|
||||
border : 2px solid #58180D;
|
||||
}
|
||||
&.sortDir{
|
||||
width : 75px;
|
||||
}
|
||||
.sort-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
color: #ccc;
|
||||
height: 100%;
|
||||
|
||||
&:hover{
|
||||
background-color : #444;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
background-color: #333;
|
||||
|
||||
button {
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
height: 100%;
|
||||
& + .sortDir {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
color: #58180D;
|
||||
|
||||
}
|
||||
&.inactive {
|
||||
color: #707070;
|
||||
|
||||
}
|
||||
&.active::before, &.inactive::before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
font-size: 0.6cm;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
&.active::before {
|
||||
content: '\f107';
|
||||
}
|
||||
&.inactive::before {
|
||||
content: '\f105';
|
||||
.filter-option {
|
||||
margin-left: 20px;
|
||||
background-color : transparent !important;
|
||||
font-size : 11px;
|
||||
i{
|
||||
padding-right : 5px;
|
||||
}
|
||||
}
|
||||
button{
|
||||
background-color : transparent;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
text-transform : uppercase;
|
||||
font-weight : normal;
|
||||
font-size : 11px;
|
||||
color : #ccc;
|
||||
padding : 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,10 @@ const EditPage = createClass({
|
||||
confirmGoogleTransfer : false,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
url : ''
|
||||
url : '',
|
||||
autoSave : true,
|
||||
autoSaveWarning : false,
|
||||
unsavedTime : new Date()
|
||||
};
|
||||
},
|
||||
savedBrew : null,
|
||||
@@ -72,9 +75,17 @@ const EditPage = createClass({
|
||||
url : window.location.href
|
||||
});
|
||||
|
||||
|
||||
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 = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
return 'You have unsaved changes!';
|
||||
@@ -117,14 +128,14 @@ const EditPage = createClass({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors
|
||||
}), ()=>this.trySave());
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style },
|
||||
isPending : true
|
||||
}), ()=>this.trySave());
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
@@ -134,7 +145,7 @@ const EditPage = createClass({
|
||||
...metadata
|
||||
},
|
||||
isPending : true,
|
||||
}), ()=>this.trySave());
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
|
||||
},
|
||||
|
||||
@@ -221,8 +232,9 @@ const EditPage = createClass({
|
||||
editId : this.savedBrew.editId,
|
||||
shareId : this.savedBrew.shareId
|
||||
},
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
unsavedTime : new Date()
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -329,17 +341,55 @@ const EditPage = createClass({
|
||||
</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){
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
}
|
||||
if(this.state.isPending && this.hasChanges()){
|
||||
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){
|
||||
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() {
|
||||
return this.state.brew.googleId && !this.state.brew.stubbed ?
|
||||
this.state.brew.googleId + this.state.brew.shareId :
|
||||
@@ -378,7 +428,10 @@ const EditPage = createClass({
|
||||
|
||||
<Nav.section>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.renderSaveButton()}
|
||||
<Nav.dropdown className='save-menu'>
|
||||
{this.renderSaveButton()}
|
||||
{this.renderAutoSaveButton()}
|
||||
</Nav.dropdown>
|
||||
<NewBrew />
|
||||
<HelpNavItem/>
|
||||
<Nav.dropdown>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
left : 50%;
|
||||
z-index : 100000;
|
||||
z-index : 500;
|
||||
width : 140px;
|
||||
padding : 3px;
|
||||
color : white;
|
||||
|
||||
@@ -38,9 +38,7 @@ const HomePage = createClass({
|
||||
},
|
||||
handleSave : function(){
|
||||
request.post('/api')
|
||||
.send({
|
||||
text : this.state.brew.text
|
||||
})
|
||||
.send(this.state.brew)
|
||||
.end((err, res)=>{
|
||||
if(err) return;
|
||||
const brew = res.body;
|
||||
|
||||
4790
package-lock.json
generated
4790
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.2.2",
|
||||
"version": "3.3.0",
|
||||
"engines": {
|
||||
"node": "16.11.x"
|
||||
},
|
||||
@@ -51,44 +51,44 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.19.1",
|
||||
"@babel/core": "^7.19.3",
|
||||
"@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",
|
||||
"body-parser": "^1.20.0",
|
||||
"body-parser": "^1.20.1",
|
||||
"classnames": "^2.3.2",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.1",
|
||||
"express": "^4.18.1",
|
||||
"express": "^4.18.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.7",
|
||||
"fs-extra": "10.1.0",
|
||||
"googleapis": "107.0.0",
|
||||
"googleapis": "108.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "4.1.0",
|
||||
"marked": "4.1.1",
|
||||
"marked-extended-tables": "^1.0.5",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^6.6.1",
|
||||
"mongoose": "^6.6.5",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.0",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-frame-component": "4.1.3",
|
||||
"react-router-dom": "6.3.0",
|
||||
"react-router-dom": "6.4.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^6.1.0",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-react": "^7.31.8",
|
||||
"jest": "^29.0.3",
|
||||
"supertest": "^6.2.4"
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"jest": "^29.2.1",
|
||||
"supertest": "^6.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +181,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'lastViewed',
|
||||
'thumbnail',
|
||||
'tags'
|
||||
];
|
||||
|
||||
|
||||
@@ -147,8 +147,7 @@ const GoogleActions = {
|
||||
editId : brew.editId || nanoid(12),
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true,
|
||||
thumbnail : brew.thumbnail
|
||||
isStubbed : true
|
||||
}
|
||||
},
|
||||
media : {
|
||||
@@ -186,8 +185,7 @@ const GoogleActions = {
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true,
|
||||
version : 1,
|
||||
thumbnail : brew.thumbnail || ''
|
||||
version : 1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -267,7 +265,6 @@ const GoogleActions = {
|
||||
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
||||
version : parseInt(obj.data.properties.version) || 0,
|
||||
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
||||
thumbnail : obj.data.properties.thumbnail || '',
|
||||
|
||||
googleId : id
|
||||
};
|
||||
|
||||
@@ -108,7 +108,7 @@ const excludePropsFromUpdate = (brew)=>{
|
||||
|
||||
const excludeGoogleProps = (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) {
|
||||
delete modified[prop];
|
||||
}
|
||||
|
||||
@@ -229,6 +229,15 @@ const CodeEditor = createClass({
|
||||
this.codeMirror.replaceSelection('\n\\page\n\n', 'end');
|
||||
},
|
||||
|
||||
injectText : function(injectText, overwrite=true) {
|
||||
const cm = this.codeMirror;
|
||||
if(!overwrite) {
|
||||
cm.setCursor(cm.getCursor('from'));
|
||||
}
|
||||
cm.replaceSelection(injectText, 'end');
|
||||
cm.focus();
|
||||
},
|
||||
|
||||
makeUnderline : function() {
|
||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
|
||||
this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
|
||||
@@ -355,12 +364,20 @@ const CodeEditor = createClass({
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
const maxLength = 50;
|
||||
|
||||
let foldPreviewText = '';
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
text += this.codeMirror.getLine(currentLine);
|
||||
if(currentLine < to.line)
|
||||
text += ' ';
|
||||
currentLine += 1;
|
||||
const currentText = this.codeMirror.getLine(currentLine);
|
||||
currentLine++;
|
||||
if(currentText[0] == '#'){
|
||||
foldPreviewText = currentText;
|
||||
break;
|
||||
}
|
||||
if(!foldPreviewText && currentText != '\n') {
|
||||
foldPreviewText = currentText;
|
||||
}
|
||||
}
|
||||
text = foldPreviewText || `Lines ${from.line+1}-${to.line+1}`;
|
||||
|
||||
text = text.trim();
|
||||
if(text.length > maxLength)
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
font-family: inherit;
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.sourceMoveFlash .CodeMirror-line{
|
||||
animation-name: sourceMoveAnimation;
|
||||
|
||||
@@ -69,7 +69,8 @@ const SplitPane = createClass({
|
||||
this.setState({ isDragging: false });
|
||||
},
|
||||
|
||||
handleDown : function(){
|
||||
handleDown : function(e){
|
||||
e.preventDefault();
|
||||
this.setState({ isDragging: true });
|
||||
//this.unFocus()
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
--HB_Color_HeaderUnderline : #C0AD6A; // Gold
|
||||
--HB_Color_HorizontalRule : #9C2B1B; // 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_WatercolorStain : #BBAD82; // Light brown
|
||||
--HB_Color_Footnotes : #C9AD6A; // Gold
|
||||
@@ -213,7 +213,9 @@ body {
|
||||
tbody{
|
||||
tr{
|
||||
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){
|
||||
background-color : var(--HB_Color_Accent);
|
||||
@@ -657,72 +659,78 @@ body {
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.page .toc{
|
||||
.page {
|
||||
&:has(.toc):after {
|
||||
display: none;
|
||||
}
|
||||
.toc {
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
h1 {
|
||||
text-align : center;
|
||||
margin-bottom : 0.3cm;
|
||||
}
|
||||
a{
|
||||
display : inline;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
h1 {
|
||||
text-align : center;
|
||||
margin-bottom : 0.3cm;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
margin-top : 0.2cm;
|
||||
line-height : 0.4cm;
|
||||
& + ul li {
|
||||
line-height: 1.2em;
|
||||
a{
|
||||
display : inline;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
li + li h3 {
|
||||
margin-top : 0.26cm;
|
||||
line-height : 1em
|
||||
h4 {
|
||||
margin-top : 0.2cm;
|
||||
line-height : 0.4cm;
|
||||
& + ul li {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
}
|
||||
h3 span:first-child::after {
|
||||
border : none;
|
||||
}
|
||||
span {
|
||||
display : table-cell;
|
||||
&:first-child {
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
&::after {
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
margin-top : 0;
|
||||
a {
|
||||
width : 100%;
|
||||
display : flex;
|
||||
flex-flow : row nowrap;
|
||||
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 : "";
|
||||
position : absolute;
|
||||
bottom : 0.08cm;
|
||||
margin-left : 0.06cm; /* Spacing before dot leaders */
|
||||
width : 100%;
|
||||
flex : 1;
|
||||
margin-left : 0.08cm; /* Spacing before dot leaders */
|
||||
margin-right : 0.16cm;
|
||||
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 {
|
||||
font-family : BookInsanityRemake;
|
||||
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*/
|
||||
margin-left : 1em;
|
||||
}
|
||||
}
|
||||
ul { /*List indent*/
|
||||
margin-left : 1em;
|
||||
&.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
&.wide{
|
||||
.useColumns(0.96, @fillMode: balance);
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
|
||||
Reference in New Issue
Block a user