mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-24 20:42:43 +00:00
Merge branch 'master' into WatercolorImageMask
This commit is contained in:
@@ -27,7 +27,7 @@ jobs:
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- node/install-npm
|
||||
- run: sudo npm install -g npm@8.10.0
|
||||
- node/install-packages:
|
||||
app-dir: ~/homebrewery
|
||||
cache-path: node_modules
|
||||
@@ -55,13 +55,13 @@ jobs:
|
||||
at: .
|
||||
|
||||
# run tests!
|
||||
- run:
|
||||
- run:
|
||||
name: Test - Basic
|
||||
command: npm run test:basic
|
||||
- run:
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-span
|
||||
- run:
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
|
||||
@@ -71,4 +71,4 @@ workflows:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
- build
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
7
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
contact_links:
|
||||
- name: /r/Homebrewery Subreddit
|
||||
url: https://www.reddit.com/r/homebrewery
|
||||
about: The Homebrewery community on Reddit!
|
||||
- name: Discord of Many Things
|
||||
url: https://discord.gg/domt
|
||||
about: "Join the conversation in the #formatting channel on DoMT!"
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Feature Request
|
||||
description: Have an idea to improve the Homebrewery? Let us know!
|
||||
labels: ["feature request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "We'd love to hear your idea! Please be sure to [search the current Issues](https://github.com/naturalcrit/homebrewery/issues) for any duplicate requests."
|
||||
- type: textarea
|
||||
id: user-request
|
||||
attributes:
|
||||
label: "Your idea:"
|
||||
description: The best feature requests provide an explanation of the current issue and then an explanation of how it could be improved. Screenshots/images can be pasted right in as well!
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Please be sure to search for any close matches to your request in the GitHub Issues tracker before opening a new request, thanks!"
|
||||
55
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
Normal file
55
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: General Issue
|
||||
description: Report an issue unrelated to Saving
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please include as much information as possible.
|
||||
- type: dropdown
|
||||
id: renderer
|
||||
attributes:
|
||||
label: Renderer
|
||||
description: Which renderer does this issue occur on? If you are unsure, you can check the renderer in the Properties Editor (click the "i" in the Snippet Menu bar above the editor).
|
||||
options:
|
||||
- v3
|
||||
- Legacy
|
||||
- Both
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Which browser were you using when the issue occurred?
|
||||
options:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- Edge
|
||||
- Safari
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: Which OS were you using when the issue occurred?
|
||||
options:
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: user-description
|
||||
attributes:
|
||||
label: "What happened?"
|
||||
description: Please include any steps you took leading up to the issue and if you can reproduce it. Let us know what you expected to happen, and what did happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: code
|
||||
attributes:
|
||||
label: Code
|
||||
description: Paste in any relevant code snippet below.
|
||||
render: gfm
|
||||
26
.github/ISSUE_TEMPLATE/save_issue.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/save_issue.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Saving Issue
|
||||
description: Report an issue Saving
|
||||
labels: ["Saving"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Woops, sorry there was an issue Saving. Please add any detail you can to this report and check back soon!
|
||||
- type: textarea
|
||||
id: error-code
|
||||
attributes:
|
||||
label: Error Code
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: user-description
|
||||
attributes:
|
||||
label: "Your description of what happened:"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the report. Here are some steps that may help in the meantime:
|
||||
1. Refreshing your Google credentials in Homebrewery by signing out, and back in (they expire after one year).
|
||||
2. Waiting a few minutes and trying again - sometimes there is just a momentary blip in the server.
|
||||
3. Check the Issues in Github or the /r/homebrewery subreddit to see if others are experiencing the same issue.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ config/docker.*
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
.vscode
|
||||
|
||||
81
changelog.md
81
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,81 @@ pre {
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Thursday 28/10/2022 - v3.3.1
|
||||
{{taskList
|
||||
|
||||
##### Calculuschild
|
||||
|
||||
* [x] Fixes to several broken CSS styles from v3.3.0
|
||||
|
||||
Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
||||
|
||||
##### Jeddai
|
||||
|
||||
* [x] Reduce size of thumbnails on social media links
|
||||
|
||||
}}
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
@@ -109,7 +109,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>;
|
||||
},
|
||||
|
||||
@@ -117,7 +122,7 @@ const BrewRenderer = createClass({
|
||||
if(!this.state.usePPR) return;
|
||||
|
||||
return <div className='ppr_msg'>
|
||||
Partial Page Renderer enabled, because your brew is so large. May effect rendering.
|
||||
Partial Page Renderer is enabled, because your brew is so large. May affect rendering.
|
||||
</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){
|
||||
@@ -154,9 +137,17 @@ const Editor = createClass({
|
||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||
}
|
||||
|
||||
// Highlight injectors {style}
|
||||
if(line.includes('{') && line.includes('}')){
|
||||
const regex = /(?<!{){(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
||||
let match;
|
||||
while ((match = regex.exec(line)) != null) {
|
||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].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 +166,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' });
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
font-weight : bold;
|
||||
//font-style: italic;
|
||||
}
|
||||
.injection{
|
||||
color : green;
|
||||
font-weight : bold;
|
||||
}
|
||||
}
|
||||
|
||||
.brewJump{
|
||||
|
||||
@@ -9,6 +9,7 @@ const Nav = require('naturalcrit/nav/nav.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'];
|
||||
|
||||
@@ -22,6 +23,7 @@ const MetadataEditor = createClass({
|
||||
editId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
thumbnail : '',
|
||||
tags : [],
|
||||
published : false,
|
||||
authors : [],
|
||||
@@ -51,11 +53,30 @@ const MetadataEditor = createClass({
|
||||
},
|
||||
|
||||
handleFieldChange : function(name, e){
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
[name] : e.target.value
|
||||
});
|
||||
e.persist();
|
||||
|
||||
// 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){
|
||||
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.
|
||||
console.log(validationErr);
|
||||
const errMessage = validationErr.map((err)=>{
|
||||
return `- ${err}`;
|
||||
}).join('\n');
|
||||
e.target.setCustomValidity(errMessage);
|
||||
e.target.reportValidity();
|
||||
};
|
||||
},
|
||||
|
||||
handleSystem : function(system, e){
|
||||
if(e.target.checked){
|
||||
this.props.metadata.systems.push(system);
|
||||
@@ -64,6 +85,7 @@ const MetadataEditor = createClass({
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
handleRenderer : function(renderer, e){
|
||||
if(e.target.checked){
|
||||
this.props.metadata.renderer = renderer;
|
||||
@@ -228,24 +250,28 @@ const MetadataEditor = createClass({
|
||||
<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 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 defaultValue={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text'
|
||||
defaultValue={this.props.metadata.thumbnail}
|
||||
placeholder='https://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,47 @@
|
||||
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;
|
||||
&:invalid {
|
||||
background : #ffb9b9;
|
||||
}
|
||||
}
|
||||
input[type='text'], textarea {
|
||||
border : 1px solid gray;
|
||||
}
|
||||
&.thumbnail{
|
||||
height : 1.4em;
|
||||
@@ -43,22 +67,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;
|
||||
|
||||
34
client/homebrew/editor/metadataEditor/validations.js
Normal file
34
client/homebrew/editor/metadataEditor/validations.js
Normal 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';
|
||||
}
|
||||
}
|
||||
],
|
||||
language : [
|
||||
(value)=>{
|
||||
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null;
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -61,24 +62,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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
height : 100%;
|
||||
background-color : @steel;
|
||||
flex-direction : column;
|
||||
overflow-y : hidden;
|
||||
.content{
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
overflow-y : hidden;
|
||||
}
|
||||
&.listPage .content {
|
||||
overflow-y : scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,12 +70,20 @@ 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'
|
||||
>
|
||||
brews
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='account'
|
||||
color='orange'
|
||||
icon='fas fa-user'
|
||||
href='/account'
|
||||
>
|
||||
account
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='logout'
|
||||
color='red'
|
||||
|
||||
@@ -12,14 +12,20 @@ module.exports = function(props){
|
||||
</Nav.item>
|
||||
<Nav.item color='red' icon='fas fa-fw fa-bug'
|
||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent`
|
||||
**Browser(s)** :
|
||||
**Operating System** :
|
||||
**Legacy or v3 Renderer** :
|
||||
**Issue** : `)}`}
|
||||
- **Browser(s)** :
|
||||
- **Operating System** :
|
||||
- **Legacy or v3 Renderer** :
|
||||
- **Issue** : `)}`}
|
||||
newTab={true}
|
||||
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}
|
||||
|
||||
@@ -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;
|
||||
@@ -101,8 +115,36 @@
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
border-top : 1px solid #888;
|
||||
overflow : clip;
|
||||
.clear{
|
||||
display : none;
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
right : 0px;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
background-color : #333;
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
margin : 0;
|
||||
height :100%;
|
||||
width :100%;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background-color : @blue;
|
||||
|
||||
.clear{
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.title{
|
||||
display : inline-block;
|
||||
|
||||
@@ -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>;
|
||||
});
|
||||
};
|
||||
|
||||
71
client/homebrew/pages/accountPage/accountPage.jsx
Normal file
71
client/homebrew/pages/accountPage/accountPage.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
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() {
|
||||
// console.log(this.props.uiItems);
|
||||
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.fileCount || '-'}</p> : '' }
|
||||
</div>
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <UIPage brew={this.props.brew}>
|
||||
{this.renderUiItems()}
|
||||
</UIPage>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AccountPage;
|
||||
@@ -101,6 +101,10 @@ const BrewItem = createClass({
|
||||
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>
|
||||
@@ -113,7 +117,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>
|
||||
</> : <></>
|
||||
|
||||
@@ -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;
|
||||
@@ -27,73 +26,101 @@
|
||||
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{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
38
client/homebrew/pages/basePages/uiPage/uiPage.jsx
Normal file
38
client/homebrew/pages/basePages/uiPage/uiPage.jsx
Normal 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;
|
||||
47
client/homebrew/pages/basePages/uiPage/uiPage.less
Normal file
47
client/homebrew/pages/basePages/uiPage/uiPage.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')) ?? 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 +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()
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -322,24 +334,62 @@ const EditPage = createClass({
|
||||
<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)}`}>
|
||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</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;
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
module.exports = async(name, title = '', props = {})=>{
|
||||
const HOMEBREWERY_PUBLIC_URL=props.config.publicUrl;
|
||||
const template = async function(name, title='', props = {}){
|
||||
const ogTags = [];
|
||||
const ogMeta = props.ogMeta ?? {};
|
||||
Object.entries(ogMeta).forEach(([key, value])=>{
|
||||
if(!value) return;
|
||||
const tag = `<meta property="og:${key}" content="${value}">`;
|
||||
ogTags.push(tag);
|
||||
});
|
||||
const ogMetaTags = ogTags.join('\n');
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||
<meta property="og:title" content="${props.brew?.title || 'Homebrewery - Untitled Brew'}">
|
||||
<meta property="og:url" content="${HOMEBREWERY_PUBLIC_URL}/${props.brew?.shareId ? `share/${props.brew.shareId}` : ''}">
|
||||
<meta property="og:image" content="${props.brew?.thumbnail || `${HOMEBREWERY_PUBLIC_URL}/thumbnail.png`}">
|
||||
<meta property="og:description" content="${props.brew?.description || 'No description.'}">
|
||||
<meta property="og:site_name" content="The Homebrewery - Make your Homebrew content look legit!">
|
||||
<meta property="og:type" content="article">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||
${ogMetaTags}
|
||||
<meta name="twitter:card" content="summary">
|
||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = template;
|
||||
19219
package-lock.json
generated
19219
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
package.json
33
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.1",
|
||||
"engines": {
|
||||
"node": "16.11.x"
|
||||
},
|
||||
@@ -51,44 +51,45 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.19.0",
|
||||
"@babel/plugin-transform-runtime": "^7.18.10",
|
||||
"@babel/preset-env": "^7.19.0",
|
||||
"@babel/core": "^7.19.6",
|
||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||
"@babel/preset-env": "^7.19.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"body-parser": "^1.20.0",
|
||||
"classnames": "^2.3.1",
|
||||
"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",
|
||||
"dedent-tabs": "^0.10.2",
|
||||
"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": "109.0.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "4.1.0",
|
||||
"marked": "4.2.3",
|
||||
"marked-extended-tables": "^1.0.5",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.4",
|
||||
"mongoose": "^6.6.0",
|
||||
"mongoose": "^6.7.0",
|
||||
"nanoid": "3.3.4",
|
||||
"nconf": "^0.12.0",
|
||||
"npm": "^8.10.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.3",
|
||||
"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.7",
|
||||
"jest": "^29.0.3",
|
||||
"supertest": "^6.2.4"
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"jest": "^29.2.2",
|
||||
"supertest": "^6.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
133
server/app.js
133
server/app.js
@@ -1,4 +1,4 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// Set working directory to project root
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
@@ -77,6 +77,14 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
const defaultMetaTags = {
|
||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
||||
title : 'The Homebrewery',
|
||||
description : 'A NaturalCrit Tool for Homebrews',
|
||||
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
|
||||
type : 'website'
|
||||
};
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||
@@ -87,17 +95,29 @@ app.get('/', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page v3
|
||||
//Home page Legacy
|
||||
app.get('/legacy', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : welcomeTextLegacy,
|
||||
renderer : 'legacy'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Homepage (Legacy)',
|
||||
description : 'Homepage'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -107,7 +127,13 @@ app.get('/migrate', (req, res, next)=>{
|
||||
req.brew = {
|
||||
text : migrateText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'v3 Migration Guide',
|
||||
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -118,7 +144,14 @@ app.get('/changelog', async (req, res, next)=>{
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'Changelog',
|
||||
description : 'Development changelog.',
|
||||
thumbnail : null
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -129,7 +162,13 @@ app.get('/faq', async (req, res, next)=>{
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3'
|
||||
},
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'FAQ',
|
||||
description : 'Frequently Asked Questions'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
return next();
|
||||
});
|
||||
@@ -167,6 +206,13 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `${req.params.username}'s Collection`,
|
||||
description : 'View my collection of homebrew on the Homebrewery.',
|
||||
image : null
|
||||
// type : could be 'profile'?
|
||||
};
|
||||
|
||||
const fields = [
|
||||
'googleId',
|
||||
'title',
|
||||
@@ -180,6 +226,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'lastViewed',
|
||||
'thumbnail',
|
||||
'tags'
|
||||
];
|
||||
|
||||
@@ -223,6 +270,14 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || null,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
sanitizeBrew(req.brew, 'edit');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
@@ -234,6 +289,13 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
sanitizeBrew(req.brew, 'share');
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
req.brew.title = `CLONE - ${req.brew.title}`;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : 'New',
|
||||
description : 'Start crafting your homebrew on the Homebrewery!',
|
||||
image : null
|
||||
};
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
@@ -241,6 +303,13 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||
const { brew } = req;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : req.brew.title || 'Untitled Brew',
|
||||
description : req.brew.description || 'No description.',
|
||||
image : req.brew.thumbnail || null,
|
||||
type : 'article'
|
||||
};
|
||||
|
||||
if(req.params.id.length > 12 && !brew._id) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
@@ -261,6 +330,61 @@ app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||
next();
|
||||
});
|
||||
|
||||
//Account Page
|
||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
data.title = 'Account Information Page';
|
||||
|
||||
let auth;
|
||||
let files;
|
||||
if(req.account) {
|
||||
if(req.account.googleId) {
|
||||
try {
|
||||
auth = await GoogleActions.authCheck(req.account, res);
|
||||
} catch (e) {
|
||||
auth = undefined;
|
||||
console.log('Google auth check failed!');
|
||||
console.log(e);
|
||||
}
|
||||
if(auth.credentials.access_token) {
|
||||
try {
|
||||
files = await GoogleActions.listGoogleBrews(auth);
|
||||
} catch (e) {
|
||||
files = undefined;
|
||||
console.log('List Google files failed!');
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||
const brews = await HomebrewModel.find(query, 'id')
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
data.uiItems = {
|
||||
username : req.account.username,
|
||||
issued : req.account.issued,
|
||||
mongoCount : brews.length,
|
||||
googleId : Boolean(req.account.googleId),
|
||||
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||
fileCount : files?.length || '-'
|
||||
};
|
||||
}
|
||||
|
||||
req.brew = data;
|
||||
|
||||
req.ogMeta = { ...defaultMetaTags,
|
||||
title : `Account Page`,
|
||||
description : null,
|
||||
image : null
|
||||
};
|
||||
|
||||
return next();
|
||||
}));
|
||||
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
// Local only
|
||||
@@ -275,8 +399,6 @@ if(isLocalEnvironment){
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
@@ -295,7 +417,8 @@ app.use(asyncHandler(async (req, res, next)=>{
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3'),
|
||||
enable_themes : config.get('enable_themes'),
|
||||
config : configuration
|
||||
config : configuration,
|
||||
ogMeta : req.ogMeta
|
||||
};
|
||||
const title = req.brew ? req.brew.title : '';
|
||||
const page = await templateFn('homebrew', title, props)
|
||||
|
||||
@@ -125,8 +125,7 @@ const GoogleActions = {
|
||||
description : file.description,
|
||||
views : parseInt(file.properties.views),
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
systems : [],
|
||||
thumbnail : file.properties.thumbnail
|
||||
systems : []
|
||||
};
|
||||
});
|
||||
return brews;
|
||||
@@ -146,8 +145,7 @@ const GoogleActions = {
|
||||
editId : brew.editId || nanoid(12),
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true,
|
||||
thumbnail : brew.thumbnail
|
||||
isStubbed : true
|
||||
}
|
||||
},
|
||||
media : {
|
||||
@@ -185,8 +183,7 @@ const GoogleActions = {
|
||||
pageCount : brew.pageCount,
|
||||
renderer : brew.renderer || 'legacy',
|
||||
isStubbed : true,
|
||||
version : 1,
|
||||
thumbnail : brew.thumbnail || ''
|
||||
version : 1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -265,7 +262,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;
|
||||
|
||||
@@ -32,7 +32,7 @@ const mustacheSpans = {
|
||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||
const inlineRegex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
||||
const inlineRegex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
||||
const match = completeSpan.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -82,7 +82,7 @@ const mustacheDivs = {
|
||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||
const blockRegex = /^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/gm;
|
||||
const blockRegex = /^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||
const match = completeBlock.exec(src);
|
||||
if(match) {
|
||||
//Find closing delimiter
|
||||
@@ -130,7 +130,7 @@ const mustacheInjectInline = {
|
||||
level : 'inline',
|
||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/g;
|
||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
@@ -165,7 +165,7 @@ const mustacheInjectBlock = {
|
||||
level : 'block',
|
||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||
tokenizer(src, tokens) {
|
||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/ym;
|
||||
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/ym;
|
||||
const match = inlineRegex.exec(src);
|
||||
if(match) {
|
||||
const lastToken = tokens[tokens.length - 1];
|
||||
|
||||
@@ -69,7 +69,8 @@ const SplitPane = createClass({
|
||||
this.setState({ isDragging: false });
|
||||
},
|
||||
|
||||
handleDown : function(){
|
||||
handleDown : function(e){
|
||||
e.preventDefault();
|
||||
this.setState({ isDragging: true });
|
||||
//this.unFocus()
|
||||
},
|
||||
|
||||
@@ -9,8 +9,8 @@ describe('Tests for static pages', ()=>{
|
||||
return app.get('/').expect(200);
|
||||
});
|
||||
|
||||
it('Home page v3 works', ()=>{
|
||||
return app.get('/v3_preview').expect(200);
|
||||
it('Home page legacy works', ()=>{
|
||||
return app.get('/legacy').expect(200);
|
||||
});
|
||||
|
||||
it('Changelog page works', ()=>{
|
||||
|
||||
@@ -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
|
||||
@@ -77,9 +77,12 @@ body {
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
}
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
|
||||
.page{
|
||||
p{
|
||||
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
|
||||
display : block;
|
||||
@@ -155,9 +158,9 @@ body {
|
||||
padding-bottom : 2px;
|
||||
margin-bottom : -20px;
|
||||
background-image : linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
background-clip : text;
|
||||
-webkit-background-clip : text;
|
||||
color : rgba(0, 0, 0, 0);
|
||||
background-clip : text;
|
||||
-webkit-background-clip : text;
|
||||
color : rgba(0, 0, 0, 0);
|
||||
}
|
||||
&+p::first-line{
|
||||
font-variant : small-caps;
|
||||
@@ -175,12 +178,24 @@ body {
|
||||
font-size : 0.575cm;
|
||||
border-bottom : 2px solid var(--HB_Color_HeaderUnderline);;
|
||||
line-height : 0.995em; //Font is misaligned. Shift up slightly
|
||||
& + * {
|
||||
margin-top: 0.17cm;
|
||||
}
|
||||
}
|
||||
* + h3 {
|
||||
margin-top : 0.155cm; //(0.325 - 0.17)
|
||||
}
|
||||
h4{
|
||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||
//margin-bottom : 0.02cm;
|
||||
font-size : 0.458cm;
|
||||
line-height : 0.971em; //Font is misaligned. Shift up slightly
|
||||
& + * {
|
||||
margin-top: 0.09cm;
|
||||
}
|
||||
}
|
||||
* + h4 {
|
||||
margin-top : 0.235cm; //(0.325 - 0.09)
|
||||
}
|
||||
h5{
|
||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||
@@ -199,6 +214,7 @@ body {
|
||||
table{
|
||||
.useSansSerif();
|
||||
width : 100%;
|
||||
line-height : 16px;
|
||||
& + * {
|
||||
margin-top : 0.325cm;
|
||||
}
|
||||
@@ -207,13 +223,17 @@ body {
|
||||
font-weight : 800;
|
||||
th{
|
||||
vertical-align : bottom;
|
||||
padding : 0.14em 0.4em;
|
||||
//padding : 0.14em 0.4em;
|
||||
padding : 0px 1.5px; // Both of these are temporary, just to force
|
||||
//line-height : 16px; // PDF to render at same height until Chrome 108
|
||||
}
|
||||
}
|
||||
tbody{
|
||||
tr{
|
||||
td{
|
||||
padding : 0.14em 0.4em;
|
||||
//padding : 0.14em 0.4em;
|
||||
padding : 0px 1.5px; // Both of these are temporary, just to force
|
||||
//line-height : 16px; // PDF to render at same height until Chrome 108
|
||||
}
|
||||
&:nth-child(odd){
|
||||
background-color : var(--HB_Color_Accent);
|
||||
@@ -625,6 +645,9 @@ body {
|
||||
&.wide:first-child {
|
||||
margin-top: 0.12cm;
|
||||
}
|
||||
& + * {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
&.decoration {
|
||||
position:relative;
|
||||
@@ -657,72 +680,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);
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
@@ -755,26 +784,13 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * BLANK LINE
|
||||
// *****************************/
|
||||
.page {
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.page .wide{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
display : block;
|
||||
margin-bottom : 0.34cm;
|
||||
&+* {
|
||||
margin-top : 0;
|
||||
}
|
||||
margin-bottom : 0.325cm;
|
||||
}
|
||||
|
||||
.page h1 + *{
|
||||
margin-top : 0;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ body {
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
:where(.page){
|
||||
.page{
|
||||
p{
|
||||
overflow-wrap : break-word;
|
||||
display : block;
|
||||
@@ -77,13 +77,7 @@ body {
|
||||
img{
|
||||
z-index : -1;
|
||||
}
|
||||
:not(:where(.wide,.columnSplit,.blank,hr)) + :where(h1,h2,h3,h4,h5,h6,table,dl,.block) {
|
||||
margin-top : 1em; //NOTE: MAKE ALL MARGINS TOP-ONLY FOR BEST RESULTS WITH COLUMN BREAKS. USE * + * STYLE SELECTORS
|
||||
}
|
||||
|
||||
:where(h1,h3,h3,h4,h5,h6) + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
//*****************************
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
@@ -116,6 +110,9 @@ body {
|
||||
font-weight : bold;
|
||||
}
|
||||
}
|
||||
div:not(.columnWrapper) > table + table { // Side-by-side tables should not
|
||||
margin-top : 0; // have vertical spacing.
|
||||
}
|
||||
|
||||
/* Watermark */
|
||||
.watermark {
|
||||
@@ -288,6 +285,10 @@ body {
|
||||
-webkit-column-break-after : always;
|
||||
break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
//Avoid breaking up
|
||||
blockquote,table{
|
||||
@@ -311,13 +312,11 @@ body {
|
||||
//*****************************
|
||||
// * MUSTACHE DIVS/SPANS
|
||||
// *****************************/
|
||||
:where(.page) {
|
||||
.page {
|
||||
.block {
|
||||
break-inside : avoid;
|
||||
display : inline-block;
|
||||
}
|
||||
& :where(.block) {
|
||||
width : 100%;
|
||||
width : 100%;
|
||||
}
|
||||
.inline-block {
|
||||
display : inline-block;
|
||||
@@ -328,7 +327,7 @@ body {
|
||||
//*****************************
|
||||
// * DEFINITION LISTS
|
||||
// *****************************/
|
||||
:where(.page) {
|
||||
.page {
|
||||
dl {
|
||||
padding-left : 1em;
|
||||
white-space : pre-line;
|
||||
@@ -348,17 +347,20 @@ body {
|
||||
//*****************************
|
||||
// * BLANK LINE
|
||||
// *****************************/
|
||||
:where(.page) {
|
||||
.page {
|
||||
.blank {
|
||||
height : 1em;
|
||||
margin-top : 0;
|
||||
& + * {
|
||||
margin-top : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
:where(.page) {
|
||||
.page {
|
||||
.wide{
|
||||
column-span : all;
|
||||
display : block;
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
:where(.page){
|
||||
.page{
|
||||
color : var(--HB_Color_Text);
|
||||
font-family : ReenieBeanie;
|
||||
font-size : 0.53cm;
|
||||
@@ -554,6 +554,6 @@
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
:where(.page) .wide {
|
||||
.page .wide {
|
||||
margin-bottom : 0.45cm;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user