0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-02 21:32:42 +00:00

Merge branch 'master' into WatercolorImageMask

This commit is contained in:
Trevor Buckner
2022-11-23 15:51:49 -05:00
44 changed files with 13827 additions and 6869 deletions

View File

@@ -27,7 +27,7 @@ jobs:
# fallback to using the latest cache if no exact match is found # fallback to using the latest cache if no exact match is found
- v1-dependencies- - v1-dependencies-
- node/install-npm - run: sudo npm install -g npm@8.10.0
- node/install-packages: - node/install-packages:
app-dir: ~/homebrewery app-dir: ~/homebrewery
cache-path: node_modules cache-path: node_modules
@@ -55,13 +55,13 @@ jobs:
at: . at: .
# run tests! # run tests!
- run: - run:
name: Test - Basic name: Test - Basic
command: npm run test:basic command: npm run test:basic
- run: - run:
name: Test - Mustache Spans name: Test - Mustache Spans
command: npm run test:mustache-span command: npm run test:mustache-span
- run: - run:
name: Test - Routes name: Test - Routes
command: npm run test:route command: npm run test:route
@@ -71,4 +71,4 @@ workflows:
- build - build
- test: - test:
requires: requires:
- build - build

7
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View 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!"

View 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!"

View 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
View 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
View File

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

View File

@@ -1,6 +1,7 @@
```css ```css
h5 { h5 {
font-size: .35cm !important; font-size: .35cm !important;
margin-top: 0.3cm;
} }
.page ul ul { .page ul ul {
@@ -14,6 +15,11 @@ h5 {
filter: brightness(1.1) drop-shadow(1px 2px 1px #222); filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
} }
.taskList ul {
margin-bottom: 0px;
margin-top: 0px;
}
.taskList li input[checked] { .taskList li input[checked] {
filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222); filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222);
} }
@@ -39,6 +45,81 @@ pre {
## changelog ## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### 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 ### Friday 08/09/2022 - v3.2.2
{{taskList {{taskList

View File

@@ -109,7 +109,12 @@ const BrewRenderer = createClass({
renderPageInfo : function(){ renderPageInfo : function(){
return <div className='pageInfo' ref='main'> return <div className='pageInfo' ref='main'>
{this.state.viewablePageNumber + 1} / {this.state.pages.length} <div>
{this.props.renderer}
</div>
<div>
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div>
</div>; </div>;
}, },
@@ -117,7 +122,7 @@ const BrewRenderer = createClass({
if(!this.state.usePPR) return; if(!this.state.usePPR) return;
return <div className='ppr_msg'> 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>; </div>;
}, },

View File

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

View File

@@ -19,11 +19,6 @@ const DEFAULT_STYLE_TEXT = dedent`
color: black; color: black;
}`; }`;
const splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index);
};
const Editor = createClass({ const Editor = createClass({
displayName : 'Editor', displayName : 'Editor',
@@ -80,19 +75,7 @@ const Editor = createClass({
}, },
handleInject : function(injectText){ handleInject : function(injectText){
let text; this.refs.codeEditor?.injectText(injectText, false);
if(this.isText()) text = this.props.brew.text;
if(this.isStyle()) text = this.props.brew.style ?? DEFAULT_STYLE_TEXT;
const lines = text.split('\n');
const cursorPos = this.refs.codeEditor.getCursorPosition();
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
const injectLines = injectText.split('\n');
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectLines.length, cursorPos.ch + injectLines[injectLines.length - 1].length);
if(this.isText()) this.props.onTextChange(lines.join('\n'));
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
}, },
handleViewChange : function(newView){ handleViewChange : function(newView){
@@ -154,9 +137,17 @@ const Editor = createClass({
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit'); 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}} // Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){ if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g; const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
let match; let match;
let blockCount = 0; let blockCount = 0;
while ((match = regex.exec(line)) != null) { while ((match = regex.exec(line)) != null) {
@@ -175,7 +166,7 @@ const Editor = createClass({
// Highlight block divs {{\n Content \n}} // Highlight block divs {{\n Content \n}}
let endCh = line.length+1; let endCh = line.length+1;
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/); const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
if(match) if(match)
endCh = match.index+match[0].length; endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' }); codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });

View File

@@ -29,6 +29,10 @@
font-weight : bold; font-weight : bold;
//font-style: italic; //font-style: italic;
} }
.injection{
color : green;
font-weight : bold;
}
} }
.brewJump{ .brewJump{

View File

@@ -9,6 +9,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx'); const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
const Themes = require('themes/themes.json'); const Themes = require('themes/themes.json');
const validations = require('./validations.js')
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']; const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
@@ -22,6 +23,7 @@ const MetadataEditor = createClass({
editId : null, editId : null,
title : '', title : '',
description : '', description : '',
thumbnail : '',
tags : [], tags : [],
published : false, published : false,
authors : [], authors : [],
@@ -51,11 +53,30 @@ const MetadataEditor = createClass({
}, },
handleFieldChange : function(name, e){ handleFieldChange : function(name, e){
this.props.onChange({ e.persist();
...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){
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){ handleSystem : function(system, e){
if(e.target.checked){ if(e.target.checked){
this.props.metadata.systems.push(system); this.props.metadata.systems.push(system);
@@ -64,6 +85,7 @@ const MetadataEditor = createClass({
} }
this.props.onChange(this.props.metadata); this.props.onChange(this.props.metadata);
}, },
handleRenderer : function(renderer, e){ handleRenderer : function(renderer, e){
if(e.target.checked){ if(e.target.checked){
this.props.metadata.renderer = renderer; this.props.metadata.renderer = renderer;
@@ -228,24 +250,28 @@ const MetadataEditor = createClass({
<div className='field title'> <div className='field title'>
<label>title</label> <label>title</label>
<input type='text' className='value' <input type='text' className='value'
value={this.props.metadata.title} defaultValue={this.props.metadata.title}
onChange={(e)=>this.handleFieldChange('title', e)} /> onChange={(e)=>this.handleFieldChange('title', e)} />
</div> </div>
<div className='field description'> <div className='field-group'>
<label>description</label> <div className='field-column'>
<textarea value={this.props.metadata.description} className='value' <div className='field description'>
onChange={(e)=>this.handleFieldChange('description', e)} /> <label>description</label>
</div> <textarea defaultValue={this.props.metadata.description} className='value'
<div className='field thumbnail'> onChange={(e)=>this.handleFieldChange('description', e)} />
<label>thumbnail</label> </div>
<input type='text' <div className='field thumbnail'>
value={this.props.metadata.thumbnail} <label>thumbnail</label>
placeholder='my.thumbnail.url' <input type='text'
className='value' defaultValue={this.props.metadata.thumbnail}
onChange={(e)=>this.handleFieldChange('thumbnail', e)} /> placeholder='https://my.thumbnail.url'
<button className='display' onClick={this.toggleThumbnailDisplay}> className='value'
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} /> onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
</button> <button className='display' onClick={this.toggleThumbnailDisplay}>
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
</button>
</div>
</div>
{this.renderThumbnail()} {this.renderThumbnail()}
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -6,10 +6,15 @@
height : 100%; height : 100%;
background-color : @steel; background-color : @steel;
flex-direction : column; flex-direction : column;
overflow-y : hidden;
.content{ .content{
position : relative; position : relative;
height : calc(~"100% - 29px"); //Navbar height height : calc(~"100% - 29px"); //Navbar height
flex : auto; flex : auto;
overflow-y : hidden;
}
&.listPage .content {
overflow-y : scroll;
} }
} }
} }

View File

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

View File

@@ -12,14 +12,20 @@ module.exports = function(props){
</Nav.item> </Nav.item>
<Nav.item color='red' icon='fas fa-fw fa-bug' <Nav.item color='red' icon='fas fa-fw fa-bug'
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent` href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent`
**Browser(s)** : - **Browser(s)** :
**Operating System** : - **Operating System** :
**Legacy or v3 Renderer** : - **Legacy or v3 Renderer** :
**Issue** : `)}`} - **Issue** : `)}`}
newTab={true} newTab={true}
rel='noopener noreferrer'> rel='noopener noreferrer'>
report issue report issue
</Nav.item> </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' <Nav.item color='blue' icon='fas fa-fw fa-file-import'
href='/migrate' href='/migrate'
newTab={true} newTab={true}

View File

@@ -55,6 +55,18 @@
text-align : center; text-align : center;
text-transform : initial; text-transform : initial;
} }
.save-menu {
.dropdown {
z-index: 1000;
}
.navItem i.fa-power-off {
color : red;
&.active {
color : rgb(0, 182, 52);
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765))
}
}
}
.patreon.navItem{ .patreon.navItem{
border-left : 1px solid #666; border-left : 1px solid #666;
border-right : 1px solid #666; border-right : 1px solid #666;
@@ -78,6 +90,8 @@
width : 100%; width : 100%;
overflow : hidden auto; overflow : hidden auto;
max-height : ~"calc(100vh - 28px)"; max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333;
scrollbar-width : thin;
h4{ h4{
display : block; display : block;
box-sizing : border-box; box-sizing : border-box;
@@ -101,8 +115,36 @@
color : white; color : white;
text-decoration : none; text-decoration : none;
border-top : 1px solid #888; 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{ &:hover{
background-color : @blue; background-color : @blue;
.clear{
display : grid;
place-content : center;
}
} }
.title{ .title{
display : inline-block; display : inline-block;

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(){ renderDropdown : function(){
if(!this.state.showDropdown) return null; 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 ]'}> 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='title'>{brew.title || '[ no title ]'}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</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>; </a>;
}); });
}; };

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

View File

@@ -101,6 +101,10 @@ const BrewItem = createClass({
const dateFormatString = 'YYYY-MM-DD HH:mm:ss'; const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
return <div className='brewItem'> return <div className='brewItem'>
{brew.thumbnail &&
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
</div>
}
<div className='text'> <div className='text'>
<h2>{brew.title}</h2> <h2>{brew.title}</h2>
<p className='description'>{brew.description}</p> <p className='description'>{brew.description}</p>
@@ -113,7 +117,7 @@ const BrewItem = createClass({
<i className='fas fa-tags'/> <i className='fas fa-tags'/>
{brew.tags.map((tag, idx)=>{ {brew.tags.map((tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/); const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
return <span className={matches[1]}>{matches[2]}</span>; return <span key={idx} className={matches[1]}>{matches[2]}</span>;
})} })}
</div> </div>
</> : <></> </> : <></>

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -1,28 +1,31 @@
module.exports = async(name, title = '', props = {})=>{ const template = async function(name, title='', props = {}){
const HOMEBREWERY_PUBLIC_URL=props.config.publicUrl; 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 ` return `<!DOCTYPE html>
<!DOCTYPE html> <html>
<html> <head>
<head> <link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<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="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> <link href=${`/${name}/bundle.css`} rel='stylesheet' />
<link href=${`/${name}/bundle.css`} rel='stylesheet' /> <link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" /> ${ogMetaTags}
<meta property="og:title" content="${props.brew?.title || 'Homebrewery - Untitled Brew'}"> <meta name="twitter:card" content="summary">
<meta property="og:url" content="${HOMEBREWERY_PUBLIC_URL}/${props.brew?.shareId ? `share/${props.brew.shareId}` : ''}"> <title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
<meta property="og:image" content="${props.brew?.thumbnail || `${HOMEBREWERY_PUBLIC_URL}/thumbnail.png`}"> </head>
<meta property="og:description" content="${props.brew?.description || 'No description.'}"> <body>
<meta property="og:site_name" content="The Homebrewery - Make your Homebrew content look legit!"> <main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
<meta property="og:type" content="article"> <script src=${`/${name}/bundle.js`}></script>
<meta name="twitter:card" content="summary_large_image"> <script>start_app(${JSON.stringify(props)})</script>
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title> </body>
</head> </html>
<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

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.2.2", "version": "3.3.1",
"engines": { "engines": {
"node": "16.11.x" "node": "16.11.x"
}, },
@@ -51,44 +51,45 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.19.0", "@babel/core": "^7.19.6",
"@babel/plugin-transform-runtime": "^7.18.10", "@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.19.0", "@babel/preset-env": "^7.19.4",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.18.6",
"body-parser": "^1.20.0", "body-parser": "^1.20.1",
"classnames": "^2.3.1", "classnames": "^2.3.2",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.1", "dedent-tabs": "^0.10.2",
"express": "^4.18.1", "express": "^4.18.2",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7", "express-static-gzip": "2.1.7",
"fs-extra": "10.1.0", "fs-extra": "10.1.0",
"googleapis": "107.0.0", "googleapis": "109.0.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6", "jwt-simple": "^0.5.6",
"less": "^3.13.1", "less": "^3.13.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "4.1.0", "marked": "4.2.3",
"marked-extended-tables": "^1.0.5", "marked-extended-tables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4", "moment": "^2.29.4",
"mongoose": "^6.6.0", "mongoose": "^6.7.0",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.0", "nconf": "^0.12.0",
"npm": "^8.10.0",
"react": "^16.14.0", "react": "^16.14.0",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"react-frame-component": "4.1.3", "react-frame-component": "4.1.3",
"react-router-dom": "6.3.0", "react-router-dom": "6.4.3",
"sanitize-filename": "1.6.3", "sanitize-filename": "1.6.3",
"superagent": "^6.1.0", "superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.23.1", "eslint": "^8.28.0",
"eslint-plugin-react": "^7.31.7", "eslint-plugin-react": "^7.31.11",
"jest": "^29.0.3", "jest": "^29.2.2",
"supertest": "^6.2.4" "supertest": "^6.3.1"
} }
} }

View File

@@ -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 // Set working directory to project root
process.chdir(`${__dirname}/..`); 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);}; 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 //Robots.txt
app.get('/robots.txt', (req, res)=>{ app.get('/robots.txt', (req, res)=>{
return res.sendFile(`robots.txt`, { root: process.cwd() }); return res.sendFile(`robots.txt`, { root: process.cwd() });
@@ -87,17 +95,29 @@ app.get('/', (req, res, next)=>{
req.brew = { req.brew = {
text : welcomeText, text : welcomeText,
renderer : 'V3' renderer : 'V3'
},
req.ogMeta = { ...defaultMetaTags,
title : 'Homepage',
description : 'Homepage'
}; };
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
return next(); return next();
}); });
//Home page v3 //Home page Legacy
app.get('/legacy', (req, res, next)=>{ app.get('/legacy', (req, res, next)=>{
req.brew = { req.brew = {
text : welcomeTextLegacy, text : welcomeTextLegacy,
renderer : 'legacy' renderer : 'legacy'
},
req.ogMeta = { ...defaultMetaTags,
title : 'Homepage (Legacy)',
description : 'Homepage'
}; };
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
return next(); return next();
}); });
@@ -107,7 +127,13 @@ app.get('/migrate', (req, res, next)=>{
req.brew = { req.brew = {
text : migrateText, text : migrateText,
renderer : 'V3' renderer : 'V3'
},
req.ogMeta = { ...defaultMetaTags,
title : 'v3 Migration Guide',
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
}; };
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
return next(); return next();
}); });
@@ -118,7 +144,14 @@ app.get('/changelog', async (req, res, next)=>{
title : 'Changelog', title : 'Changelog',
text : changelogText, text : changelogText,
renderer : 'V3' renderer : 'V3'
},
req.ogMeta = { ...defaultMetaTags,
title : 'Changelog',
description : 'Development changelog.',
thumbnail : null
}; };
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
return next(); return next();
}); });
@@ -129,7 +162,13 @@ app.get('/faq', async (req, res, next)=>{
title : 'FAQ', title : 'FAQ',
text : faqText, text : faqText,
renderer : 'V3' renderer : 'V3'
},
req.ogMeta = { ...defaultMetaTags,
title : 'FAQ',
description : 'Frequently Asked Questions'
}; };
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
return next(); return next();
}); });
@@ -167,6 +206,13 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
app.get('/user/:username', async (req, res, next)=>{ app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username); 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 = [ const fields = [
'googleId', 'googleId',
'title', 'title',
@@ -180,6 +226,7 @@ app.get('/user/:username', async (req, res, next)=>{
'createdAt', 'createdAt',
'updatedAt', 'updatedAt',
'lastViewed', 'lastViewed',
'thumbnail',
'tags' 'tags'
]; ];
@@ -223,6 +270,14 @@ app.get('/user/:username', async (req, res, next)=>{
//Edit Page //Edit Page
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew; 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'); sanitizeBrew(req.brew, 'edit');
splitTextStyleAndMetadata(req.brew); 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. 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'); sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew); splitTextStyleAndMetadata(req.brew);
req.brew.title = `CLONE - ${req.brew.title}`; req.brew.title = `CLONE - ${req.brew.title}`;
req.ogMeta = { ...defaultMetaTags,
title : 'New',
description : 'Start crafting your homebrew on the Homebrewery!',
image : null
};
return next(); 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)=>{ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
const { brew } = req; 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) { if(req.params.id.length > 12 && !brew._id) {
const googleId = req.params.id.slice(0, -12); const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12); const shareId = req.params.id.slice(-12);
@@ -261,6 +330,61 @@ app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
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 nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv); const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
// Local only // Local only
@@ -275,8 +399,6 @@ if(isLocalEnvironment){
}); });
} }
//Render the page //Render the page
const templateFn = require('./../client/template.js'); const templateFn = require('./../client/template.js');
app.use(asyncHandler(async (req, res, next)=>{ app.use(asyncHandler(async (req, res, next)=>{
@@ -295,7 +417,8 @@ app.use(asyncHandler(async (req, res, next)=>{
account : req.account, account : req.account,
enable_v3 : config.get('enable_v3'), enable_v3 : config.get('enable_v3'),
enable_themes : config.get('enable_themes'), enable_themes : config.get('enable_themes'),
config : configuration config : configuration,
ogMeta : req.ogMeta
}; };
const title = req.brew ? req.brew.title : ''; const title = req.brew ? req.brew.title : '';
const page = await templateFn('homebrew', title, props) const page = await templateFn('homebrew', title, props)

View File

@@ -125,8 +125,7 @@ const GoogleActions = {
description : file.description, description : file.description,
views : parseInt(file.properties.views), views : parseInt(file.properties.views),
published : file.properties.published ? file.properties.published == 'true' : false, published : file.properties.published ? file.properties.published == 'true' : false,
systems : [], systems : []
thumbnail : file.properties.thumbnail
}; };
}); });
return brews; return brews;
@@ -146,8 +145,7 @@ const GoogleActions = {
editId : brew.editId || nanoid(12), editId : brew.editId || nanoid(12),
pageCount : brew.pageCount, pageCount : brew.pageCount,
renderer : brew.renderer || 'legacy', renderer : brew.renderer || 'legacy',
isStubbed : true, isStubbed : true
thumbnail : brew.thumbnail
} }
}, },
media : { media : {
@@ -185,8 +183,7 @@ const GoogleActions = {
pageCount : brew.pageCount, pageCount : brew.pageCount,
renderer : brew.renderer || 'legacy', renderer : brew.renderer || 'legacy',
isStubbed : true, isStubbed : true,
version : 1, version : 1
thumbnail : brew.thumbnail || ''
} }
}; };
@@ -265,7 +262,6 @@ const GoogleActions = {
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
version : parseInt(obj.data.properties.version) || 0, version : parseInt(obj.data.properties.version) || 0,
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy', renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
thumbnail : obj.data.properties.thumbnail || '',
googleId : id googleId : id
}; };

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ const mustacheSpans = {
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token 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); const match = completeSpan.exec(src);
if(match) { if(match) {
//Find closing delimiter //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 start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token 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); const match = completeBlock.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -130,7 +130,7 @@ const mustacheInjectInline = {
level : 'inline', level : 'inline',
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/g; const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -165,7 +165,7 @@ const mustacheInjectBlock = {
level : 'block', level : 'block',
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/ym; const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/ym;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];

View File

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

View File

@@ -9,8 +9,8 @@ describe('Tests for static pages', ()=>{
return app.get('/').expect(200); return app.get('/').expect(200);
}); });
it('Home page v3 works', ()=>{ it('Home page legacy works', ()=>{
return app.get('/v3_preview').expect(200); return app.get('/legacy').expect(200);
}); });
it('Changelog page works', ()=>{ it('Changelog page works', ()=>{

View File

@@ -8,7 +8,7 @@
--HB_Color_HeaderUnderline : #C0AD6A; // Gold --HB_Color_HeaderUnderline : #C0AD6A; // Gold
--HB_Color_HorizontalRule : #9C2B1B; // Maroon --HB_Color_HorizontalRule : #9C2B1B; // Maroon
--HB_Color_HeaderText : #58180D; // Dark Maroon --HB_Color_HeaderText : #58180D; // Dark Maroon
--HB_Color_MonsterStatBackground : #EEDBAB; // Light orange parchment --HB_Color_MonsterStatBackground : #F2E5B5; // Light orange parchment
--HB_Color_CaptionText : #766649; // Brown --HB_Color_CaptionText : #766649; // Brown
--HB_Color_WatercolorStain : #BBAD82; // Light brown --HB_Color_WatercolorStain : #BBAD82; // Light brown
--HB_Color_Footnotes : #C9AD6A; // Gold --HB_Color_Footnotes : #C9AD6A; // Gold
@@ -77,9 +77,12 @@ body {
text-rendering : optimizeLegibility; text-rendering : optimizeLegibility;
page-break-before : always; page-break-before : always;
page-break-after : always; page-break-after : always;
}
//***************************** //*****************************
// * BASE // * BASE
// *****************************/ // *****************************/
.page{
p{ p{
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
display : block; display : block;
@@ -155,9 +158,9 @@ body {
padding-bottom : 2px; padding-bottom : 2px;
margin-bottom : -20px; margin-bottom : -20px;
background-image : linear-gradient(-45deg, #322814, #998250, #322814); background-image : linear-gradient(-45deg, #322814, #998250, #322814);
background-clip : text; background-clip : text;
-webkit-background-clip : text; -webkit-background-clip : text;
color : rgba(0, 0, 0, 0); color : rgba(0, 0, 0, 0);
} }
&+p::first-line{ &+p::first-line{
font-variant : small-caps; font-variant : small-caps;
@@ -175,12 +178,24 @@ body {
font-size : 0.575cm; font-size : 0.575cm;
border-bottom : 2px solid var(--HB_Color_HeaderUnderline);; border-bottom : 2px solid var(--HB_Color_HeaderUnderline);;
line-height : 0.995em; //Font is misaligned. Shift up slightly line-height : 0.995em; //Font is misaligned. Shift up slightly
& + * {
margin-top: 0.17cm;
}
}
* + h3 {
margin-top : 0.155cm; //(0.325 - 0.17)
} }
h4{ h4{
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly //margin-top : -0.02cm; //Font is misaligned. Shift up slightly
//margin-bottom : 0.02cm; //margin-bottom : 0.02cm;
font-size : 0.458cm; font-size : 0.458cm;
line-height : 0.971em; //Font is misaligned. Shift up slightly line-height : 0.971em; //Font is misaligned. Shift up slightly
& + * {
margin-top: 0.09cm;
}
}
* + h4 {
margin-top : 0.235cm; //(0.325 - 0.09)
} }
h5{ h5{
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly //margin-top : -0.02cm; //Font is misaligned. Shift up slightly
@@ -199,6 +214,7 @@ body {
table{ table{
.useSansSerif(); .useSansSerif();
width : 100%; width : 100%;
line-height : 16px;
& + * { & + * {
margin-top : 0.325cm; margin-top : 0.325cm;
} }
@@ -207,13 +223,17 @@ body {
font-weight : 800; font-weight : 800;
th{ th{
vertical-align : bottom; 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{ tbody{
tr{ tr{
td{ 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){ &:nth-child(odd){
background-color : var(--HB_Color_Accent); background-color : var(--HB_Color_Accent);
@@ -625,6 +645,9 @@ body {
&.wide:first-child { &.wide:first-child {
margin-top: 0.12cm; margin-top: 0.12cm;
} }
& + * {
margin-top: 0;
}
} }
&.decoration { &.decoration {
position:relative; position:relative;
@@ -657,72 +680,78 @@ body {
//***************************** //*****************************
// * TABLE OF CONTENTS // * TABLE OF CONTENTS
// *****************************/ // *****************************/
.page .toc{ .page {
&:has(.toc):after {
display: none;
}
.toc {
-webkit-column-break-inside : avoid; -webkit-column-break-inside : avoid;
page-break-inside : avoid; page-break-inside : avoid;
break-inside : avoid; break-inside : avoid;
h1 { h1 {
text-align : center; text-align : center;
margin-bottom : 0.3cm; margin-bottom : 0.3cm;
}
a{
display : inline;
color : inherit;
text-decoration : none;
&:hover{
text-decoration : underline;
} }
} a{
h4 { display : inline;
margin-top : 0.2cm; color : inherit;
line-height : 0.4cm; text-decoration : none;
& + ul li { &:hover{
line-height: 1.2em; text-decoration : underline;
}
} }
} h4 {
ul{ margin-top : 0.2cm;
padding-left : 0; line-height : 0.4cm;
list-style-type : none; & + ul li {
li + li h3 { line-height: 1.2em;
margin-top : 0.26cm; }
line-height : 1em
} }
h3 span:first-child::after { ul{
border : none; padding-left : 0;
} list-style-type : none;
span { margin-top : 0;
display : table-cell; a {
&:first-child { width : 100%;
position : relative; display : flex;
overflow : hidden; flex-flow : row nowrap;
&::after { justify-content : space-between;
}
li + li h3 {
margin-top : 0.26cm;
line-height : 1em
}
h3 span:first-child::after {
border : none;
}
span {
display : contents;
&:first-child::after {
content : ""; content : "";
position : absolute;
bottom : 0.08cm; bottom : 0.08cm;
margin-left : 0.06cm; /* Spacing before dot leaders */ flex : 1;
width : 100%; margin-left : 0.08cm; /* Spacing before dot leaders */
margin-right : 0.16cm;
border-bottom : 0.05cm dotted #000; border-bottom : 0.05cm dotted #000;
margin-bottom : 0.08cm;
}
&:last-child {
display : inline-block;
align-self : flex-end;
font-family : "BookInsanityRemake";
font-size : 0.34cm;
font-weight : normal;
color : #000;
} }
} }
&:last-child { ul { /*List indent*/
font-family : BookInsanityRemake; margin-left : 1em;
font-size : 0.34cm;
font-weight : normal;
color : black;
text-align : right;
vertical-align : bottom; /* Keep page number bottom-aligned */
width : 1%;
padding-left : 0.06cm; /* Spacing after dot leaders */
/*white-space : nowrap; /* Uncomment if needed */
} }
} }
ul { /*List indent*/ &.wide{
margin-left : 1em; .useColumns(0.96, @fillMode: balance);
} }
} }
&.wide{
.useColumns(0.96, @fillMode: balance);
}
} }
//***************************** //*****************************
@@ -755,26 +784,13 @@ body {
} }
} }
//*****************************
// * BLANK LINE
// *****************************/
.page {
.blank {
height : 1em;
margin-top : 0;
}
}
//***************************** //*****************************
// * WIDE // * WIDE
// *****************************/ // *****************************/
.page .wide{ .page .wide{
column-span : all; margin-bottom : 0.325cm;
-webkit-column-span : all; }
-moz-column-span : all;
display : block; .page h1 + *{
margin-bottom : 0.34cm; margin-top : 0;
&+* {
margin-top : 0;
}
} }

View File

@@ -43,7 +43,7 @@ body {
//***************************** //*****************************
// * BASE // * BASE
// *****************************/ // *****************************/
:where(.page){ .page{
p{ p{
overflow-wrap : break-word; overflow-wrap : break-word;
display : block; display : block;
@@ -77,13 +77,7 @@ body {
img{ img{
z-index : -1; 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 // * HEADERS
// *****************************/ // *****************************/
@@ -116,6 +110,9 @@ body {
font-weight : bold; font-weight : bold;
} }
} }
div:not(.columnWrapper) > table + table { // Side-by-side tables should not
margin-top : 0; // have vertical spacing.
}
/* Watermark */ /* Watermark */
.watermark { .watermark {
@@ -288,6 +285,10 @@ body {
-webkit-column-break-after : always; -webkit-column-break-after : always;
break-after : always; break-after : always;
-moz-column-break-after : always; -moz-column-break-after : always;
margin-top : 0;
& + * {
margin-top : 0;
}
} }
//Avoid breaking up //Avoid breaking up
blockquote,table{ blockquote,table{
@@ -311,13 +312,11 @@ body {
//***************************** //*****************************
// * MUSTACHE DIVS/SPANS // * MUSTACHE DIVS/SPANS
// *****************************/ // *****************************/
:where(.page) { .page {
.block { .block {
break-inside : avoid; break-inside : avoid;
display : inline-block; display : inline-block;
} width : 100%;
& :where(.block) {
width : 100%;
} }
.inline-block { .inline-block {
display : inline-block; display : inline-block;
@@ -328,7 +327,7 @@ body {
//***************************** //*****************************
// * DEFINITION LISTS // * DEFINITION LISTS
// *****************************/ // *****************************/
:where(.page) { .page {
dl { dl {
padding-left : 1em; padding-left : 1em;
white-space : pre-line; white-space : pre-line;
@@ -348,17 +347,20 @@ body {
//***************************** //*****************************
// * BLANK LINE // * BLANK LINE
// *****************************/ // *****************************/
:where(.page) { .page {
.blank { .blank {
height : 1em; height : 1em;
margin-top : 0; margin-top : 0;
& + * {
margin-top : 0;
}
} }
} }
//***************************** //*****************************
// * WIDE // * WIDE
// *****************************/ // *****************************/
:where(.page) { .page {
.wide{ .wide{
column-span : all; column-span : all;
display : block; display : block;

View File

@@ -62,7 +62,7 @@
//***************************** //*****************************
// * BASE // * BASE
// *****************************/ // *****************************/
:where(.page){ .page{
color : var(--HB_Color_Text); color : var(--HB_Color_Text);
font-family : ReenieBeanie; font-family : ReenieBeanie;
font-size : 0.53cm; font-size : 0.53cm;
@@ -554,6 +554,6 @@
//***************************** //*****************************
// * WIDE // * WIDE
// *****************************/ // *****************************/
:where(.page) .wide { .page .wide {
margin-bottom : 0.45cm; margin-bottom : 0.45cm;
} }