0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-03 12:42:41 +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

View File

@@ -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>;
},

View File

@@ -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;

View File

@@ -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){

View File

@@ -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>

View File

@@ -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;

View File

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

View File

@@ -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'
>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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{

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

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

View File

@@ -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;