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

Merge branch 'master' into addStyleSanitization-#1437

This commit is contained in:
G.Ambatte
2023-06-17 20:09:45 +12:00
committed by GitHub
197 changed files with 15118 additions and 14375 deletions

View File

@@ -27,6 +27,7 @@ const BrewRenderer = createClass({
style : '',
renderer : 'legacy',
theme : '5ePHB',
lang : '',
errors : []
};
},
@@ -140,7 +141,8 @@ const BrewRenderer = createClass({
renderStyle : function() {
if(!this.props.style) return;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.sanitizeScriptTags(this.props.style)} </style>` }} />;
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.sanitizeScriptTags(this.props.style)} </style>` }} />;
},
renderPage : function(pageText, index){
@@ -195,7 +197,6 @@ const BrewRenderer = createClass({
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = this.props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
return (
<React.Fragment>
{!this.state.isMounted
@@ -228,7 +229,7 @@ const BrewRenderer = createClass({
&&
<>
{this.renderStyle()}
<div className='pages' ref='pages'>
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
{this.renderPages()}
</div>
</>

View File

@@ -4,7 +4,7 @@ const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames'); //Unused variable
const DISMISS_KEY = 'dismiss_notification08-27-22';
const DISMISS_KEY = 'dismiss_notification12-04-23';
const NotificationPopup = createClass({
displayName : 'NotificationPopup',
@@ -25,21 +25,13 @@ const NotificationPopup = createClass({
return (
<>
<li key='psa'>
<em>V3.2.0 Released!</em> <br />
We are happy to announce that after nearly a year of use by our many users,
we are making the V3 render mode the default setting for all new brews.
This mode has become quite popular, and has proven to be stable and powerful.
Of course, we will always keep the option to use the Legacy renderer for any
brew, which can still be accessed from the Properties menu.
</li>
<li key='stubs'>
<em>Change to Google Drive Storage!</em> <br />
We have made a change to the process of tranferring brews between Google
Drive and the Homebrewery storage. Starting now, any time a brew is
transferred, it will keep the same links instead of generating new ones!
We hope this change will help reduce issues where people "lost" their work
by trying to visit old links.
<em>Broken default logo on <b>CoverPage</b> </em> <br />
If you have used the Cover Page snippet and notice the Naturalcrit
logo is showing as a broken image, this is due to some small tweaks
of this BETA feature. To fix the logo in your cover page, rename
the image link <b>"/assets/naturalCritLogoRed.svg"</b>. Remember
that any snippet marked "BETA" may have a similar change in the
future as we encounter any bugs or reworks.
</li>
<li key='googleDriveFolder'>

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',
@@ -37,6 +32,7 @@ const Editor = createClass({
onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
reportError : ()=>{},
renderer : 'legacy'
};
@@ -80,19 +76,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){
@@ -154,9 +138,17 @@ const Editor = createClass({
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
}
// Highlight injectors {style}
if(line.includes('{') && line.includes('}')){
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
}
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
@@ -175,7 +167,7 @@ const Editor = createClass({
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
@@ -300,7 +292,8 @@ const Editor = createClass({
rerenderParent={this.rerenderParent} />
<MetadataEditor
metadata={this.props.brew}
onChange={this.props.onMetaChange} />
onChange={this.props.onMetaChange}
reportError={this.props.reportError}/>
</>;
}
},

View File

@@ -19,16 +19,20 @@
background-color : fade(#299, 15%);
border-bottom : #299 solid 1px;
}
.block{
.block:not(.cm-comment){
color : purple;
font-weight : bold;
//font-style: italic;
}
.inline-block{
.inline-block:not(.cm-comment){
color : red;
font-weight : bold;
//font-style: italic;
}
.injection:not(.cm-comment){
color : green;
font-weight : bold;
}
}
.brewJump{

View File

@@ -4,16 +4,24 @@ const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const request = require('../../utils/request-middleware.js');
const Nav = require('naturalcrit/nav/nav.jsx');
const Combobox = require('client/components/combobox.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
const Themes = require('themes/themes.json');
const validations = require('./validations.js');
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
const homebreweryThumbnail = require('../../thumbnail.png');
const callIfExists = (val, fn, ...args)=>{
if(val[fn]) {
val[fn](...args);
}
};
const MetadataEditor = createClass({
displayName : 'MetadataEditor',
getDefaultProps : function() {
@@ -22,14 +30,17 @@ const MetadataEditor = createClass({
editId : null,
title : '',
description : '',
thumbnail : '',
tags : [],
published : false,
authors : [],
systems : [],
renderer : 'legacy',
theme : '5ePHB'
theme : '5ePHB',
lang : 'en'
},
onChange : ()=>{}
onChange : ()=>{},
reportError : ()=>{}
};
},
@@ -51,11 +62,28 @@ const MetadataEditor = createClass({
},
handleFieldChange : function(name, e){
this.props.onChange({
...this.props.metadata,
[name] : e.target.value
});
// load validation rules, and check input value against them
const inputRules = validations[name] ?? [];
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
// if no validation rules, save to props
if(validationErr.length === 0){
callIfExists(e.target, 'setCustomValidity', '');
this.props.onChange({
...this.props.metadata,
[name] : e.target.value
});
} else {
// if validation issues, display built-in browser error popup with each error.
const errMessage = validationErr.map((err)=>{
return `- ${err}`;
}).join('\n');
callIfExists(e.target, 'setCustomValidity', errMessage);
callIfExists(e.target, 'reportValidity');
}
},
handleSystem : function(system, e){
if(e.target.checked){
this.props.metadata.systems.push(system);
@@ -64,6 +92,7 @@ const MetadataEditor = createClass({
}
this.props.onChange(this.props.metadata);
},
handleRenderer : function(renderer, e){
if(e.target.checked){
this.props.metadata.renderer = renderer;
@@ -85,6 +114,11 @@ const MetadataEditor = createClass({
this.props.onChange(this.props.metadata);
},
handleLanguage : function(languageCode){
this.props.metadata.lang = languageCode;
this.props.onChange(this.props.metadata);
},
handleDelete : function(){
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;
@@ -96,8 +130,12 @@ const MetadataEditor = createClass({
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
.send()
.end(function(err, res){
window.location.href = '/';
.end((err, res)=>{
if(err) {
this.props.reportError(err);
} else {
window.location.href = '/';
}
});
},
@@ -159,6 +197,10 @@ const MetadataEditor = createClass({
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
{`${theme.renderer} : ${theme.name}`}
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
<div className='preview'>
<h6>{`${theme.name}`} preview</h6>
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
</div>
</div>;
});
};
@@ -168,14 +210,14 @@ const MetadataEditor = createClass({
if(this.props.metadata.renderer == 'legacy') {
dropdown =
<Nav.dropdown className='disabled' trigger='disabled'>
<Nav.dropdown className='disabled value' trigger='disabled'>
<div>
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
</div>
</Nav.dropdown>;
} else {
dropdown =
<Nav.dropdown trigger='click'>
<Nav.dropdown className='value' trigger='click'>
<div>
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
</div>
@@ -190,6 +232,47 @@ const MetadataEditor = createClass({
</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(){
if(!global.enable_v3) return;
@@ -225,24 +308,26 @@ const MetadataEditor = createClass({
render : function(){
return <div className='metadataEditor'>
<h1 className='sectionHead'>Brew</h1>
<div className='field title'>
<label>title</label>
<input type='text' className='value'
value={this.props.metadata.title}
defaultValue={this.props.metadata.title}
onChange={(e)=>this.handleFieldChange('title', e)} />
</div>
<div className='field-group'>
<div className='field-column'>
<div className='field description'>
<label>description</label>
<textarea value={this.props.metadata.description} className='value'
<textarea defaultValue={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'
defaultValue={this.props.metadata.thumbnail}
placeholder='https://my.thumbnail.url'
className='value'
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
<button className='display' onClick={this.toggleThumbnailDisplay}>
@@ -258,8 +343,6 @@ const MetadataEditor = createClass({
values={this.props.metadata.tags}
onChange={(e)=>this.handleFieldChange('tags', e)}/>
{this.renderAuthors()}
<div className='field systems'>
<label>systems</label>
<div className='value'>
@@ -267,10 +350,29 @@ const MetadataEditor = createClass({
</div>
</div>
{this.renderLanguageDropdown()}
{this.renderThemeDropdown()}
{this.renderRenderOptions()}
<hr/>
<h1 className='sectionHead'>Authors</h1>
{this.renderAuthors()}
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true}
values={this.props.metadata.invitedAuthors}
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)}/>
<hr/>
<h1 className='sectionHead'>Privacy</h1>
<div className='field publish'>
<label>publish</label>
<div className='value'>

View File

@@ -10,6 +10,15 @@
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
overflow-y : auto;
.sectionHead {
font-weight: 1000;
margin: 20px 0;
&:first-of-type {
margin-top: 0;
}
}
& > div {
margin-bottom: 10px;
}
@@ -26,12 +35,16 @@
flex-direction: column;
flex: 5 0 200px;
gap: 10px;
}
.field{
display : flex;
flex-wrap : wrap;
width : 100%;
min-width : 200px;
position : relative;
&>label{
width : 80px;
font-size : 11px;
@@ -42,6 +55,15 @@
&>.value{
flex : 1 1 auto;
width : 50px;
&:invalid {
background : #ffb9b9;
}
}
input[type='text'], textarea {
border : 1px solid gray;
&:focus {
outline: 1px solid #444;
}
}
&.thumbnail{
height : 1.4em;
@@ -72,6 +94,17 @@
font-size : 0.8em;
}
}
&.language .language-dropdown {
max-width : 150px;
z-index : 200;
}
small {
font-size : 0.6em;
font-style : italic;
line-height : 1.4em;
display : inline-block;
}
}
@@ -122,10 +155,6 @@
button.unpublish{
.button(@silver);
}
small{
font-size : 0.6em;
font-style : italic;
}
}
.delete.field .value{
@@ -142,9 +171,8 @@
font-size : 13.33px;
.navDropdownContainer {
background-color : white;
width : 100%;
position : relative;
z-index : 500;
z-index : 100;
&.disabled {
font-style :italic;
font-style : italic;
@@ -165,24 +193,51 @@
}
.navDropdown {
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
position : absolute;
width : 100%;
position : absolute;
width : 100%;
.item {
padding : 3px 3px;
border-top : 1px solid rgb(118, 118, 118);
position : relative;
overflow : hidden;
padding : 3px 3px;
border-top : 1px solid rgb(118, 118, 118);
position : relative;
overflow : visible;
background-color : white;
.preview {
display : flex;
flex-direction: column;
background : #ccc;
border-radius : 5px;
box-shadow : 0 0 5px black;
width : 200px;
color :black;
position : absolute;
top : 0;
right : 0;
opacity : 0;
transition : opacity 250ms ease;
z-index : 1;
overflow :hidden;
h6 {
font-weight : 900;
padding-inline:1em;
padding-block :.5em;
border-bottom :2px solid hsl(0,0%,40%);
}
}
&:hover {
background-color : @blue;
color : white;
color : white;
}
img {
mask-image : linear-gradient(90deg, transparent, black 20%);
&:hover > .preview {
opacity: 1;
}
>img {
mask-image : linear-gradient(90deg, transparent, black 20%);
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
position : absolute;
left : ~"max(100px, 100% - 300px)";
top : 0px;
position : absolute;
right : 0;
top : 0px;
width : 50%;
height : 100%;
}
}
}
@@ -190,6 +245,7 @@
}
.field .list {
display: flex;
flex: 1 0;
flex-wrap: wrap;
> * {

View File

@@ -0,0 +1,34 @@
module.exports = {
title : [
(value)=>{
return value?.length > 100 ? 'Max title length of 100 characters' : null;
}
],
description : [
(value)=>{
return value?.length > 500 ? 'Max description length of 500 characters.' : null;
}
],
thumbnail : [
(value)=>{
return value?.length > 256 ? 'Max URL length of 256 characters.' : null;
},
(value)=>{
if(value?.length == 0){return null;}
try {
Boolean(new URL(value));
return null;
} catch (e) {
return 'Must be a valid URL';
}
}
],
lang : [
(value)=>{
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;
}
]
};

View File

@@ -163,15 +163,23 @@ const SnippetGroup = createClass({
onSnippetClick : function(){},
};
},
handleSnippetClick : function(snippet){
handleSnippetClick : function(e, snippet){
e.stopPropagation();
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
},
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
renderSnippets : function(snippets){
return _.map(snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
<i className={snippet.icon} />
{snippet.name}
<span className='name'>{snippet.name}</span>
{snippet.experimental && <span className='beta'>beta</span>}
{snippet.subsnippets && <>
<i className='fas fa-caret-right'></i>
<div className='dropdown side'>
{this.renderSnippets(snippet.subsnippets)}
</div></>}
</div>;
});
},
@@ -182,7 +190,7 @@ const SnippetGroup = createClass({
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets()}
{this.renderSnippets(this.props.snippets)}
</div>
</div>;
},

View File

@@ -1,4 +1,4 @@
@import (less) './client/icons/customIcons.less';
.snippetBar{
@menuHeight : 25px;
position : relative;
@@ -83,7 +83,7 @@
.snippetGroup{
border-right : 1px solid black;
&:hover{
.dropdown{
&>.dropdown{
visibility : visible;
}
}
@@ -96,16 +96,47 @@
padding : 0px;
background-color : #ddd;
.snippet{
position: relative;
.animate(background-color);
padding : 5px;
cursor : pointer;
font-size : 10px;
display : flex;
align-items : center;
min-width : max-content;
padding : 5px;
cursor : pointer;
font-size : 10px;
i{
margin-right : 8px;
font-size : 1.2em;
height : 1.2em;
&~i{
margin-right: 0;
margin-left: 5px;
}
}
.name {
margin-right : auto;
}
.beta {
color : white;
padding : 4px 6px;
line-height : 1em;
margin-left : 5px;
align-self : center;
background : grey;
border-radius : 12px;
font-family : monospace;
}
&:hover{
background-color : #999;
&>.dropdown{
visibility : visible;
&.side {
left: 100%;
top: 0%;
margin-left:0;
box-shadow: -1px 1px 2px 0px #999;
}
}
}
}
}

View File

@@ -9,7 +9,9 @@ const StringArrayEditor = createClass({
label : '',
values : [],
valuePatterns : null,
validators : [],
placeholder : '',
notes : [],
unique : false,
cannotEdit : [],
onChange : ()=>{}
@@ -83,7 +85,8 @@ const StringArrayEditor = createClass({
}
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
const uniqueIfSet = !this.props.unique || !values.includes(value);
return matchesPatterns && uniqueIfSet;
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
return matchesPatterns && uniqueIfSet && passesValidators;
},
handleValueInputKeyDown : function(event, index) {
@@ -123,17 +126,21 @@ const StringArrayEditor = createClass({
</div>
);
return <div className='field values'>
return <div className='field'>
<label>{this.props.label}</label>
<div className='list'>
{valueElements}
<div className='input-group'>
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
value={this.state.temporaryValue}
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
<div style={{ flex: '1 0' }}>
<div className='list'>
{valueElements}
<div className='input-group'>
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
value={this.state.temporaryValue}
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
</div>
</div>
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
</div>
</div>;
}

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

@@ -11,6 +11,7 @@ const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx');
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx');
const WithRoute = (props)=>{
const params = useParams();
@@ -46,6 +47,7 @@ const Homebrew = createClass({
editId : null,
createdAt : null,
updatedAt : null,
lang : ''
}
};
},
@@ -61,24 +63,27 @@ const Homebrew = createClass({
},
render : function (){
return <Router location={this.props.url}>
<div className='homebrew'>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
<Route path='/new' element={<WithRoute el={NewPage}/>} />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
<Route path='/print' element={<WithRoute el={PrintPage} />} />
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
</Routes>
</div>
</Router>;
return (
<Router location={this.props.url}>
<div className='homebrew'>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
<Route path='/new' element={<WithRoute el={NewPage}/>} />
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
<Route path='/print' element={<WithRoute el={PrintPage} />} />
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
</Routes>
</div>
</Router>
);
}
});

View File

@@ -63,19 +63,27 @@ const Account = createClass({
if(global.account){
return <Nav.dropdown>
<Nav.item
className='account'
className='account username'
color='orange'
icon='fas fa-user'
>
{global.account.username}
</Nav.item>
<Nav.item
href={`/user/${global.account.username}`}
href={`/user/${encodeURI(global.account.username)}`}
color='yellow'
icon='fas fa-beer'
>
brews
</Nav.item>
<Nav.item
className='account'
color='orange'
icon='fas fa-user'
href='/account'
>
account
</Nav.item>
<Nav.item
className='logout'
color='red'

View File

@@ -0,0 +1,85 @@
require('./error-navitem.less');
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const createClass = require('create-react-class');
const ErrorNavItem = createClass({
getDefaultProps : function() {
return {
error : '',
parent : null
};
},
render : function() {
const clearError = ()=>{
const state = {
error : null
};
if(this.props.parent.state.isSaving) {
state.isSaving = false;
}
this.props.parent.setState(state);
};
const error = this.props.error;
const response = error.response;
const status = response.status;
const message = response.body?.message;
let errMsg = '';
try {
errMsg += `${error.toString()}\n\n`;
errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
{message ?? 'Conflict: please refresh to get latest changes'}
</div>
</Nav.item>;
} else if(status === 412) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
</div>
</Nav.item>;
}
if(response.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Looks like your Google credentials have
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
here
</a>.
</div>
</Nav.item>;
}
});
module.exports = ErrorNavItem;

View File

@@ -0,0 +1,75 @@
.navItem.error {
position : relative;
background-color : @red;
}
.errorContainer{
animation-name: glideDown;
animation-duration: 0.4s;
position : absolute;
top : 100%;
left : 50%;
z-index : 1000;
width : 140px;
padding : 3px;
color : white;
background-color : #333;
border : 3px solid #444;
border-radius : 5px;
transform : translate(-50% + 3px, 10px);
text-align : center;
font-size : 10px;
font-weight : 800;
text-transform : uppercase;
a{
color : @teal;
}
&:before {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #444;
left: 53px;
top: -23px;
}
&:after {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #333;
left: 53px;
top: -19px;
}
.deny {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
border-left : 1px solid #666;
.animate(background-color);
&:hover{
background-color : red;
}
}
.confirm {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
color : white;
.animate(background-color);
&:hover{
background-color : teal;
}
}
}

View File

@@ -20,6 +20,12 @@ module.exports = function(props){
rel='noopener noreferrer'>
report issue
</Nav.item>
<Nav.item color='green' icon='fas fa-question-circle'
href='/faq'
newTab={true}
rel='noopener noreferrer'>
FAQ
</Nav.item>
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
href='/migrate'
newTab={true}

View File

@@ -0,0 +1,90 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx');
const MetadataNav = createClass({
displayName : 'MetadataNav',
getDefaultProps : function() {
return {
};
},
getInitialState : function() {
return {
showMetaWindow : false
};
},
componentDidMount : function() {
},
toggleMetaWindow : function(){
this.setState((prevProps)=>({
showMetaWindow : !prevProps.showMetaWindow
}));
},
getAuthors : function(){
if(!this.props.brew.authors || this.props.brew.authors.length == 0) return 'No authors';
return <>
{this.props.brew.authors.map((author, idx, arr)=>{
const spacer = arr.length - 1 == idx ? <></> : <span>, </span>;
return <span key={idx}><a className='userPageLink' href={`/user/${author}`}>{author}</a>{spacer}</span>;
})}
</>;
},
getTags : function(){
if(!this.props.brew.tags || this.props.brew.tags.length == 0) return 'No tags';
return <>
{this.props.brew.tags.map((tag, idx)=>{
return <span className='tag' key={idx}>{tag}</span>;
})}
</>;
},
getSystems : function(){
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
return this.props.brew.systems.join(', ');
},
renderMetaWindow : function(){
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
<div className='row'>
<h4>Description</h4>
<p>{this.props.brew.description || 'No description.'}</p>
</div>
<div className='row'>
<h4>Authors</h4>
<p>{this.getAuthors()}</p>
</div>
<div className='row'>
<h4>Tags</h4>
<p>{this.getTags()}</p>
</div>
<div className='row'>
<h4>Systems</h4>
<p>{this.getSystems()}</p>
</div>
<div className='row'>
<h4>Updated</h4>
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
</div>
</div>;
},
render : function(){
return <Nav.item icon='fas fa-info-circle' color='grey' className='metadata'
onClick={()=>this.toggleMetaWindow()}>
{this.props.children}
{this.renderMetaWindow()}
</Nav.item>;
}
});
module.exports = MetadataNav;

View File

@@ -1,150 +1,278 @@
@import 'naturalcrit/styles/colors.less';
@navbarHeight : 28px;
@keyframes pinkColoring {
//from {color: white;}
//to {color: red;}
0% {color: pink;}
50% {color: pink;}
75% {color: red;}
100% {color: pink;}
}
.homebrew nav{
.homebrewLogo{
.animate(color);
font-family : CodeBold;
font-size : 12px;
color : white;
div{
margin-top : 2px;
margin-bottom : -2px;
}
&:hover{
color : @blue;
}
}
.editTitle.navItem{
padding : 2px 12px;
input{
width : 250px;
margin : 0;
padding : 2px;
background-color : #444;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
border : 1px solid @blue;
outline : none;
}
.charCount{
display : inline-block;
vertical-align : bottom;
margin-left : 8px;
color : #666;
text-align : right;
&.max{
color : @red;
}
}
}
.brewTitle.navItem{
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
text-transform : initial;
}
.patreon.navItem{
border-left : 1px solid #666;
border-right : 1px solid #666;
&:hover i {
color: red;
}
i{
.animate(color);
animation-name: pinkColoring;
animation-duration: 2s;
color: pink;
}
}
.recent.navItem {
position : relative;
.dropdown{
position : absolute;
top : 28px;
left : 0px;
z-index : 10000;
width : 100%;
overflow : hidden auto;
max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333;
scrollbar-width : thin;
h4{
display : block;
box-sizing : border-box;
padding : 5px 0px;
background-color : #333;
font-size : 0.8em;
color : #bbb;
text-align : center;
border-top : 1px solid #888;
&:nth-of-type(1){ background-color: darken(@teal, 20%); }
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
}
.item{
@import "naturalcrit/styles/colors.less";
@navbarHeight : 28px;
@keyframes pinkColoring {
0% {color : pink;}
50% {color : pink;}
75% {color : red;}
100% {color : pink;}
}
.homebrew nav {
.homebrewLogo {
.animate(color);
font-family : CodeBold;
font-size : 12px;
color : white;
div {
margin-top : 2px;
margin-bottom : -2px;
}
&:hover {
color : @blue;
}
}
.editTitle.navItem {
padding : 2px 12px;
input {
font-family : "Open Sans", sans-serif;
font-size : 12px;
font-weight : 800;
width : 250px;
margin : 0;
padding : 2px;
text-align : center;
color : white;
border : 1px solid @blue;
outline : none;
background-color : transparent;
}
.charCount {
display : inline-block;
margin-left : 8px;
text-align : right;
vertical-align : bottom;
color : #666;
&.max {
color : @red;
}
}
}
.brewTitle.navItem {
font-size : 12px;
font-weight : 800;
height : 100%;
text-align : center;
text-transform : initial;
color : white;
background-color : transparent;
flex-grow : 1;
}
.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-right : 1px solid #666;
border-left : 1px solid #666;
&:hover i {
color : red;
}
i {
.animate(color);
animation-name : pinkColoring;
animation-duration : 2s;
color : pink;
}
}
.recent.navItem {
position : relative;
.dropdown {
position : absolute;
z-index : 10000;
top : 28px;
left : 0;
overflow : hidden auto;
width : 100%;
max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333;
scrollbar-width : thin;
h4 {
font-size : 0.8em;
display : block;
box-sizing : border-box;
padding : 5px 0;
text-align : center;
color : #BBB;
border-top : 1px solid #888;
background-color : #333;
&:nth-of-type(1) {
background-color : darken(@teal, 20%);
}
&:nth-of-type(2) {
background-color : darken(@purple, 30%);
}
}
.item {
#backgroundColorsHover;
.animate(background-color);
position : relative;
display : block;
box-sizing : border-box;
padding : 8px 5px 13px;
background-color : #333;
color : white;
text-decoration : none;
border-top : 1px solid #888;
&:hover{
background-color : @blue;
}
.title{
display : inline-block;
overflow : hidden;
width : 100%;
text-overflow : ellipsis;
white-space : nowrap;
}
.time{
position : absolute;
right : 2px;
bottom : 2px;
font-size : 0.7em;
color : #888;
}
}
}
}
.warning.navItem{
position : relative;
background-color : @orange;
color : white;
&:hover>.dropdown{
visibility : visible;
}
.dropdown{
position : absolute;
display : block;
top : 28px;
left : 0px;
visibility : hidden;
z-index : 10000;
box-sizing : border-box;
width : 100%;
padding : 13px 5px;
background-color : #333;
text-align : center;
}
}
.account.navItem{
min-width: 100px;
}
}
.animate(background-color);
position : relative;
display : block;
overflow : clip;
box-sizing : border-box;
padding : 8px 5px 13px;
text-decoration : none;
color : white;
border-top : 1px solid #888;
background-color : #333;
.clear {
position : absolute;
top : 50%;
right : 0;
display : none;
width : 20px;
height : 100%;
transform : translateY(-50%);
opacity : 70%;
border-radius : 3px;
background-color : #333;
&:hover {
opacity : 100%;
}
i {
font-size : 10px;
width : 100%;
height : 100%;
margin : 0;
text-align : center;
}
}
&:hover {
background-color : @blue;
.clear {
display : grid;
place-content : center;
}
}
.title {
display : inline-block;
overflow : hidden;
width : 100%;
white-space : nowrap;
text-overflow : ellipsis;
}
.time {
font-size : 0.7em;
position : absolute;
right : 2px;
bottom : 2px;
color : #888;
}
}
}
}
.metadata.navItem {
position : relative;
display : flex;
align-items : center;
height : 100%;
padding : 0;
flex-grow : 1;
i {
margin-right : 10px;
}
.window {
position : absolute;
z-index : -1;
bottom : 0;
left : 50%;
display : flex;
justify-content : flex-start;
width : 440px;
max-height : ~"calc(100vh - 28px)";
margin : 0 auto;
padding : 0 10px 5px;
transition : transform 0.4s, opacity 0.4s;
border : 3px solid #444;
border-top : unset;
border-radius : 0 0 5px 5px;
background-color : #333;
box-shadow : inset 0 7px 9px -7px #111;
flex-flow : row wrap;
align-content : baseline;
&.active {
transform : translateX(-50%) translateY(100%);
opacity : 1;
}
&.inactive {
transform : translateX(-50%) translateY(0%);
opacity : 0;
}
.row {
display : flex;
width : 100%;
flex-flow : row wrap;
h4 {
display : block;
box-sizing : border-box;
min-width : 76px;
padding : 5px 0;
text-align : center;
color : #BBB;
flex-basis : 20%;
flex-grow : 1;
}
p {
font-family : "Open Sans", sans-serif;
font-size : 10px;
font-weight : normal;
padding : 5px 0;
text-transform : initial;
flex-basis : 80%;
flex-grow : 1;
.tag {
display : inline-block;
margin : 2px 2px;
padding : 2px;
border : 2px solid grey;
border-radius : 5px;
background-color : #444;
}
a.userPageLink {
text-decoration : none;
color : white;
&:hover {
text-decoration : underline;
}
}
}
&:nth-of-type(even) {
background-color : #555;
}
}
}
}
.warning.navItem {
position : relative;
color : white;
background-color : @orange;
&:hover > .dropdown {
visibility : visible;
}
.dropdown {
position : absolute;
z-index : 10000;
top : 28px;
left : 0;
display : block;
visibility : hidden;
box-sizing : border-box;
width : 100%;
padding : 13px 5px;
text-align : center;
background-color : #333;
}
}
.account.navItem {
min-width : 100px;
}
.account.username.navItem {
text-transform : none;
}
}

View File

@@ -119,6 +119,25 @@ const RecentItems = createClass({
});
},
removeItem : function(url, evt){
evt.preventDefault();
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
edited = edited.filter((item)=>{ return (item.url !== url);});
viewed = viewed.filter((item)=>{ return (item.url !== url);});
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
this.setState({
edit : edited,
view : viewed
});
},
renderDropdown : function(){
if(!this.state.showDropdown) return null;
@@ -127,6 +146,7 @@ const RecentItems = createClass({
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
<span className='title'>{brew.title || '[ no title ]'}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</span>
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
</a>;
});
};

View File

@@ -0,0 +1,74 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const moment = require('moment');
const UIPage = require('../basePages/uiPage/uiPage.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
const AccountPage = createClass({
displayName : 'AccountPage',
getDefaultProps : function() {
return {
brew : {},
uiItems : {}
};
},
getInitialState : function() {
return {
uiItems : this.props.uiItems
};
},
renderNavItems : function() {
return <Navbar>
<Nav.section>
<NewBrew />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>;
},
renderUiItems : function() {
return <>
<div className='dataGroup'>
<h1>Account Information <i className='fas fa-user'></i></h1>
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
</div>
<div className='dataGroup'>
<h3>Homebrewery Information <NaturalCritIcon /></h3>
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
</div>
<div className='dataGroup'>
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
{this.props.uiItems.googleId &&
<p>
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
</p>
}
</div>
</>;
},
render : function(){
return <UIPage brew={this.props.brew}>
{this.renderUiItems()}
</UIPage>;
}
});
module.exports = AccountPage;

View File

@@ -4,9 +4,9 @@ const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const moment = require('moment');
const request = require('superagent');
const request = require('../../../../utils/request-middleware.js');
const googleDriveIcon = require('../../../../googleDrive.png');
const googleDriveIcon = require('../../../../googleDrive.svg');
const dedent = require('dedent-tabs').default;
const BrewItem = createClass({
@@ -18,7 +18,8 @@ const BrewItem = createClass({
description : '',
authors : [],
stubbed : true
}
},
reportError : ()=>{}
};
},
@@ -33,8 +34,12 @@ const BrewItem = createClass({
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
.send()
.end(function(err, res){
location.reload();
.end((err, res)=>{
if(err) {
this.props.reportError(err);
} else {
location.reload();
}
});
},
@@ -117,7 +122,7 @@ const BrewItem = createClass({
<i className='fas fa-tags'/>
{brew.tags.map((tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
return <span className={matches[1]}>{matches[2]}</span>;
return <span key={idx} className={matches[1]}>{matches[2]}</span>;
})}
</div>
</> : <></>

View File

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

View File

@@ -23,7 +23,8 @@ const ListPage = createClass({
brews : []
}
],
navItems : <></>
navItems : <></>,
reportError : null
};
},
getInitialState : function() {
@@ -81,7 +82,7 @@ const ListPage = createClass({
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
return _.map(brews, (brew, idx)=>{
return <BrewItem brew={brew} key={idx}/>;
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
});
},
@@ -218,12 +219,13 @@ const ListPage = createClass({
render : function(){
return <div className='listPage sitePage'>
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
{this.props.navItems}
{this.renderSortOptions()}
<div className='content V3'>
<div className='phb page'>
<div className='page'>
{this.renderBrewCollection(this.state.brewCollection)}
</div>
</div>

View File

@@ -10,14 +10,15 @@
-moz-column-width : auto;
-webkit-column-gap : auto;
-moz-column-gap : auto;
height : auto;
min-height : 279.4mm;
margin : 20px auto;
}
.listPage{
.content{
.phb{
.noColumns();
height : auto;
min-height : 279.4mm;
margin : 20px auto;
z-index : 1;
.page{
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
&::after{
display : none;
}
@@ -26,7 +27,29 @@
font-size : 1.3em;
font-style : italic;
}
.brewCollection {
h1:hover{
cursor: pointer;
}
.active::before, .inactive::before {
font-family: 'Font Awesome 5 Free';
font-weight: 900;
font-size: 0.6cm;
padding-right: 0.5em;
}
.active {
color: var(--HB_Color_HeaderText);
}
.active::before {
content: '\f107';
}
.inactive {
color: #707070;
}
.inactive::before {
content: '\f105';
}
}
}
}
.sort-container{
@@ -41,7 +64,7 @@
border-bottom : 1px solid #666;
color : white;
text-align : center;
z-index : 500;
z-index : 1;
display : flex;
justify-content : center;
align-items : baseline;

View File

@@ -0,0 +1,38 @@
require('./uiPage.less');
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../../navbar/navbar.jsx');
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
const Account = require('../../../navbar/account.navitem.jsx');
const UIPage = createClass({
displayName : 'UIPage',
render : function(){
return <div className='uiPage sitePage'>
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
<NewBrewItem />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>
<div className='content'>
{this.props.children}
</div>
</div>;
}
});
module.exports = UIPage;

View File

@@ -0,0 +1,47 @@
.uiPage{
.content{
overflow-y : hidden;
width : 90vw;
background-color: #f0f0f0;
font-family: 'Open Sans';
margin-left: auto;
margin-right: auto;
margin-top: 25px;
padding: 2% 4%;
font-size: 0.8em;
line-height: 1.8em;
.dataGroup{
padding: 6px 20px 15px;
border: 2px solid black;
border-radius: 5px;
margin: 5px 0px;
}
h1, h2, h3, h4{
font-weight: 900;
text-transform: uppercase;
margin: 0.5em 30% 0.25em 0;
border-bottom: 2px solid slategrey;
}
h1 {
font-size: 2em;
border-bottom: 2px solid darkslategrey;
margin-bottom: 0.5em;
margin-right: 0;
}
h2 {
font-size: 1.75em;
}
h3 {
font-size: 1.5em;
svg {
width: 19px;
}
}
h4 {
font-size: 1.25em;
}
strong {
font-weight: bold;
}
}
}

View File

@@ -3,7 +3,7 @@ require('./editPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const request = require('superagent');
const request = require('../../utils/request-middleware.js');
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
@@ -12,6 +12,7 @@ const Navbar = require('../../navbar/navbar.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
@@ -21,8 +22,9 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Markdown = require('naturalcrit/markdown.js');
const googleDriveActive = require('../../googleDrive.png');
const googleDriveInactive = require('../../googleDriveMono.png');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const googleDriveIcon = require('../../googleDrive.svg');
const SAVE_TIMEOUT = 3000;
@@ -30,24 +32,7 @@ const EditPage = createClass({
displayName : 'EditPage',
getDefaultProps : function() {
return {
brew : {
text : '',
style : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
gDrive : false,
trashed : false,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : [],
renderer : 'legacy'
}
brew : DEFAULT_BREW_LOAD
};
},
@@ -60,9 +45,12 @@ const EditPage = createClass({
alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
errors : null,
error : null,
htmlErrors : Markdown.validate(this.props.brew.text),
url : ''
url : '',
autoSave : true,
autoSaveWarning : false,
unsavedTime : new Date()
};
},
savedBrew : null,
@@ -74,7 +62,14 @@ const EditPage = createClass({
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
this.trySave();
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
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 +112,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 +129,7 @@ const EditPage = createClass({
...metadata
},
isPending : true,
}), ()=>this.trySave());
}), ()=>{if(this.state.autoSave) this.trySave();});
},
@@ -161,7 +156,10 @@ const EditPage = createClass({
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}));
this.clearErrors();
this.setState({
error : null,
isSaving : false
});
},
closeAlerts : function(event){
@@ -177,24 +175,16 @@ const EditPage = createClass({
this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
isSaving : false,
errors : null
error : null
}), ()=>this.save());
},
clearErrors : function(){
this.setState({
errors : null,
isSaving : false
});
},
save : async function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
this.setState((prevState)=>({
isSaving : true,
errors : null,
error : null,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
@@ -209,8 +199,9 @@ const EditPage = createClass({
.send(brew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ errors: err });
this.setState({ error: err });
});
if(!res) return;
this.savedBrew = res.body;
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
@@ -219,19 +210,18 @@ const EditPage = createClass({
brew : { ...prevState.brew,
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
editId : this.savedBrew.editId,
shareId : this.savedBrew.shareId
shareId : this.savedBrew.shareId,
version : this.savedBrew.version
},
isPending : false,
isSaving : false,
isPending : false,
isSaving : false,
unsavedTime : new Date()
}));
},
renderGoogleDriveIcon : function(){
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
{this.state.saveGoogle
? <img src={googleDriveActive} alt='googleDriveActive'/>
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
}
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
{this.state.confirmGoogleTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
@@ -264,67 +254,28 @@ const EditPage = createClass({
</div>
</div>
}
{this.state.alertTrashedGoogleBrew &&
<div className='errorContainer' onClick={this.closeAlerts}>
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
<div className='confirm'>
OK
</div>
</div>
}
</Nav.item>;
},
renderSaveButton : function(){
if(this.state.errors){
let errMsg = '';
try {
errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
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.`;
// if(this.state.errors.status == '401'){
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
// Oops!
// <div className='errorContainer' onClick={this.clearErrors}>
// You must be signed in to a Google account
// to save this to<br />Google Drive!<br />
// <a target='_blank' rel='noopener noreferrer'
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
// <div className='confirm'>
// Sign In
// </div>
// </a>
// <div className='deny'>
// Not Now
// </div>
// </div>
// </Nav.item>;
// }
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
Looks like your Google credentials have
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' rel='noopener noreferrer'
href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
here
</a>.
{text}
</div>
</Nav.item>;
}
@@ -335,11 +286,42 @@ const EditPage = createClass({
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;
},
errorReported : function(error) {
this.setState({
error
});
},
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 :
@@ -362,23 +344,19 @@ const EditPage = createClass({
const shareLink = this.processShareId();
return <Navbar>
{this.state.alertTrashedGoogleBrew &&
<div className='errorContainer' onClick={this.closeAlerts}>
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
<div className='confirm'>
OK
</div>
</div>
}
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderGoogleDriveIcon()}
{this.renderSaveButton()}
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
<Nav.dropdown className='save-menu'>
{this.renderSaveButton()}
{this.renderAutoSaveButton()}
</Nav.dropdown>
}
<NewBrew />
<HelpNavItem/>
<Nav.dropdown>
@@ -416,9 +394,17 @@ const EditPage = createClass({
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
reportError={this.errorReported}
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>
</div>
</div>;

View File

@@ -13,87 +13,17 @@
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
}
}
.googleDriveStorage {
position : relative;
}
.googleDriveStorage img{
height : 20px;
height : 18px;
padding : 0px;
margin : -5px;
}
.errorContainer{
animation-name: glideDown;
animation-duration: 0.4s;
position : absolute;
top : 100%;
left : 50%;
z-index : 100000;
width : 140px;
padding : 3px;
color : white;
background-color : #333;
border : 3px solid #444;
border-radius : 5px;
transform : translate(-50% + 3px, 10px);
text-align : center;
font-size : 10px;
font-weight : 800;
text-transform : uppercase;
a{
color : @teal;
}
&:before {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #444;
left: 53px;
top: -23px;
}
&:after {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #333;
left: 53px;
top: -19px;
}
.deny {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
border-left : 1px solid #666;
.animate(background-color);
&:hover{
background-color : red;
}
}
.confirm {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
color : white;
.animate(background-color);
&:hover{
background-color : teal;
}
&.inactive {
filter: grayscale(1);
}
}
}

View File

@@ -3,7 +3,7 @@ const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const request = require('../../utils/request-middleware.js');
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
@@ -12,37 +12,38 @@ const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const HomePage = createClass({
displayName : 'HomePage',
getDefaultProps : function() {
return {
brew : {
text : '',
},
ver : '0.0.0'
brew : DEFAULT_BREW,
ver : '0.0.0'
};
},
getInitialState : function() {
return {
brew : this.props.brew,
welcomeText : this.props.brew.text
welcomeText : this.props.brew.text,
error : undefined
};
},
handleSave : function(){
request.post('/api')
.send({
text : this.state.brew.text
})
.send(this.state.brew)
.end((err, res)=>{
if(err) return;
if(err) {
this.setState({ error: err });
return;
}
const brew = res.body;
window.location = `/edit/${brew.editId}`;
});
@@ -58,6 +59,10 @@ const HomePage = createClass({
renderNavbar : function(){
return <Navbar ver={this.props.ver}>
<Nav.section>
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
null
}
<NewBrewItem />
<HelpNavItem />
<RecentNavItem />

View File

@@ -40,4 +40,11 @@
right : 350px;
}
}
.navItem.save{
background-color: @orange;
&:hover{
background-color: @green;
}
}
}

View File

@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
}}
![homebrew mug](http://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px}
![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px}
{{artist,bottom:160px,left:100px
##### Homebrew Mug

View File

@@ -3,13 +3,14 @@ require('./newPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const request = require('superagent');
const request = require('../../utils/request-middleware.js');
const Markdown = require('naturalcrit/markdown.js');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const HelpNavItem = require('../../navbar/help.navitem.jsx');
@@ -17,6 +18,8 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
@@ -26,36 +29,18 @@ const NewPage = createClass({
displayName : 'NewPage',
getDefaultProps : function() {
return {
brew : {
text : '',
style : undefined,
title : '',
description : '',
renderer : 'V3',
theme : '5ePHB'
}
brew : DEFAULT_BREW
};
},
getInitialState : function() {
let brew = this.props.brew;
if(this.props.brew.shareId) {
brew = {
text : brew.text ?? '',
style : brew.style ?? undefined,
title : brew.title ?? '',
description : brew.description ?? '',
renderer : brew.renderer ?? 'legacy',
theme : brew.theme ?? '5ePHB'
};
}
const brew = this.props.brew;
return {
brew : brew,
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
errors : null,
error : null,
htmlErrors : Markdown.validate(brew.text)
};
},
@@ -76,6 +61,7 @@ const NewPage = createClass({
// brew.description = metaStorage?.description || this.state.brew.description;
brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang;
this.setState({
brew : brew
@@ -83,8 +69,9 @@ const NewPage = createClass({
}
localStorage.setItem(BREWKEY, brew.text);
localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
if(brew.style)
localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
},
componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys);
@@ -128,21 +115,16 @@ const NewPage = createClass({
handleMetaChange : function(metadata){
this.setState((prevState)=>({
brew : { ...prevState.brew, ...metadata },
}));
localStorage.setItem(METAKEY, JSON.stringify({
// 'title' : this.state.brew.title,
// 'description' : this.state.brew.description,
'renderer' : this.state.brew.renderer,
'theme' : this.state.brew.theme
}));
},
clearErrors : function(){
this.setState({
errors : null,
isSaving : false
}), ()=>{
localStorage.setItem(METAKEY, JSON.stringify({
// 'title' : this.state.brew.title,
// 'description' : this.state.brew.description,
'renderer' : this.state.brew.renderer,
'theme' : this.state.brew.theme,
'lang' : this.state.brew.lang
}));
});
;
},
save : async function(){
@@ -167,7 +149,7 @@ const NewPage = createClass({
.send(brew)
.catch((err)=>{
console.log(err);
this.setState({ isSaving: false, errors: err });
this.setState({ isSaving: false, error: err });
});
if(!res) return;
@@ -179,67 +161,6 @@ const NewPage = createClass({
},
renderSaveButton : function(){
if(this.state.errors){
let errMsg = '';
try {
errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
// if(this.state.errors.status == '401'){
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
// Oops!
// <div className='errorContainer' onClick={this.clearErrors}>
// You must be signed in to a Google account
// to save this to<br />Google Drive!<br />
// <a target='_blank' rel='noopener noreferrer'
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
// <div className='confirm'>
// Sign In
// </div>
// </a>
// <div className='deny'>
// Not Now
// </div>
// </div>
// </Nav.item>;
// }
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
Looks like your Google credentials have
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' rel='noopener noreferrer'
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
here
</a>.
</div>
</Nav.item>;
}
if(this.state.isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
save...
@@ -269,7 +190,10 @@ const NewPage = createClass({
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
this.renderSaveButton()
}
{this.renderLocalPrintButton()}
<HelpNavItem />
<RecentNavItem />
@@ -291,7 +215,7 @@ const NewPage = createClass({
onMetaChange={this.handleMetaChange}
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>
</div>
</div>;

View File

@@ -4,79 +4,5 @@
&:hover{
background-color: @green;
}
&.error{
position : relative;
background-color : @red;
}
}
.errorContainer{
animation-name: glideDown;
animation-duration: 0.4s;
position : absolute;
top : 100%;
left : 50%;
z-index : 100000;
width : 140px;
padding : 3px;
color : white;
background-color : #333;
border : 3px solid #444;
border-radius : 5px;
transform : translate(-50% + 3px, 10px);
text-align : center;
font-size : 10px;
font-weight : 800;
text-transform : uppercase;
a{
color : @teal;
}
&:before {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #444;
left: 53px;
top: -23px;
}
&:after {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #333;
left: 53px;
top: -19px;
}
.deny {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
border-left : 1px solid #666;
.animate(background-color);
&:hover{
background-color : red;
}
}
.confirm {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
color : white;
.animate(background-color);
&:hover{
background-color : teal;
}
}
}
}

View File

@@ -29,9 +29,10 @@ const PrintPage = createClass({
getInitialState : function() {
return {
brew : {
text : this.props.brew.text || '',
style : this.props.brew.style || undefined,
renderer : this.props.brew.renderer || 'legacy'
text : this.props.brew.text || '',
style : this.props.brew.style || undefined,
renderer : this.props.brew.renderer || 'legacy',
theme : this.props.brew.theme || '5ePHB'
}
};
},
@@ -48,7 +49,7 @@ const PrintPage = createClass({
text : brewStorage,
style : styleStorage,
renderer : metaStorage?.renderer || 'legacy',
theme : metaStorage?.theme || '5ePHB'
theme : metaStorage?.theme || '5ePHB'
}
};
});
@@ -59,7 +60,8 @@ const PrintPage = createClass({
renderStyle : function() {
if(!this.state.brew.style) return;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.state.brew.style} </style>` }} />;
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.state.brew.style}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.state.brew.style}\n</style>` }} />;
},
renderPages : function(){
@@ -91,6 +93,7 @@ const PrintPage = createClass({
return <div>
<Meta name='robots' content='noindex, nofollow' />
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
}

View File

@@ -5,6 +5,7 @@ const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
@@ -12,21 +13,13 @@ const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const SharePage = createClass({
displayName : 'SharePage',
getDefaultProps : function() {
return {
brew : {
title : '',
text : '',
style : '',
shareId : null,
createdAt : null,
updatedAt : null,
views : 0,
renderer : ''
}
brew : DEFAULT_BREW_LOAD
};
},
@@ -58,8 +51,10 @@ const SharePage = createClass({
return <div className='sharePage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
<Nav.section className='titleSection'>
<MetadataNav brew={this.props.brew}>
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
</MetadataNav>
</Nav.section>
<Nav.section>

View File

@@ -1,4 +1,8 @@
.sharePage{
.navContent .navSection.titleSection {
flex-grow: 1;
justify-content: center;
}
.content{
overflow-y : hidden;
}

View File

@@ -12,6 +12,7 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const UserPage = createClass({
displayName : 'UserPage',
@@ -19,7 +20,8 @@ const UserPage = createClass({
return {
username : '',
brews : [],
query : ''
query : '',
error : null
};
},
getInitialState : function() {
@@ -50,10 +52,19 @@ const UserPage = createClass({
brewCollection : brewCollection
};
},
errorReported : function(error) {
this.setState({
error
});
},
navItems : function() {
return <Navbar>
<Nav.section>
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
null
}
<NewBrew />
<HelpNavItem />
<RecentNavItem />
@@ -63,7 +74,7 @@ const UserPage = createClass({
},
render : function(){
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>;
}
});

View File

@@ -0,0 +1,12 @@
const request = require('superagent');
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
const requestMiddleware = {
get : (path)=>addHeader(request.get(path)),
put : (path)=>addHeader(request.put(path)),
post : (path)=>addHeader(request.post(path)),
delete : (path)=>addHeader(request.delete(path)),
};
module.exports = requestMiddleware;