0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-24 22:52:40 +00:00

Merge branch 'master' into SwappableThemes-ReorganizeFolderStructure

This commit is contained in:
Trevor Buckner
2022-02-10 23:36:41 -05:00
33 changed files with 2561 additions and 1796 deletions

View File

@@ -34,16 +34,37 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Wednesday 02/02/2022 - v3.0.7
{{taskList
* [x] Revert active line highlighting.
Fixes issues: [#1913](https://github.com/naturalcrit/homebrewery/issues/1913)
* [x] Added install steps for Ubuntu. [HERE](https://github.com/naturalcrit/homebrewery/blob/master/install/README.UBUNTU.md)
Fixes issues: [#1900](https://github.com/naturalcrit/homebrewery/issues/1900)
* [x] Added social media links to home page.
* [x] Increase brews visible on the user page to 1,000.
Fixes issues: [#1943](https://github.com/naturalcrit/homebrewery/issues/1943)
* [x] Added a Legacy to V3 migration guide under **NEED HELP? {{fa,fa-question-circle}} → MIGRATE {{fas,fa-file-import}}**
* [x] Background refactoring and unit tests.
}}
### Saturday 18/12/2021 - v3.0.6
{{taskList
* [x] Fixed text wrapping for long strings in code blocks.
Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736)
* [x] Code search/replace `CTRL F / CTRL SHIFT F`
Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201)
* [x] Auto-closing HTML tags and curly braces `{{ }}`
* [x] Highlight current active line
@@ -56,7 +77,7 @@ For a full record of development, visit our [Github Page](https://github.com/nat
* [x] Make columns even in V3 Table of Contents.
Fixes issues: [#1671](https://github.com/naturalcrit/homebrewery/issues/1671)
* [x] Fix `CTRL P` failing to print from `/new` pages.
Fixes issues: [#1815](https://github.com/naturalcrit/homebrewery/issues/1815)

View File

@@ -0,0 +1,25 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.dropdown>
<Nav.item color='grey' icon='fas fa-question-circle'>
need help?
</Nav.item>
<Nav.item color='red' icon='fas fa-fw fa-bug'
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`}
newTab={true}
rel='noopener noreferrer'>
report issue
</Nav.item>
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
href='/migrate'
newTab={true}
rel='noopener noreferrer'>
migrate
</Nav.item>
</Nav.dropdown>;
};

View File

@@ -1,13 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.item
newTab={true}
color='red'
icon='fas fa-bug'
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
report issue
</Nav.item>;
};

View File

@@ -1,5 +1,6 @@
@import 'naturalcrit/styles/colors.less';
@navbarHeight : 28px;
@keyframes coloring {
@keyframes pinkColoring {
//from {color: white;}
//to {color: red;}
0% {color: pink;}
@@ -62,19 +63,21 @@
}
i{
.animate(color);
animation-name: coloring;
animation-name: pinkColoring;
animation-duration: 2s;
color: pink;
}
}
.recent.navItem{
.recent.navItem {
position : relative;
.dropdown{
position : absolute;
top : 28px;
left : 0px;
z-index : 10000;
width : 100%;
position : absolute;
top : 28px;
left : 0px;
z-index : 10000;
width : 100%;
overflow : hidden auto;
max-height : ~"calc(100vh - 28px)";
h4{
display : block;
box-sizing : border-box;
@@ -88,11 +91,12 @@
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
}
.item{
#backgroundColors;
.animate(background-color);
position : relative;
display : block;
box-sizing : border-box;
padding : 13px 5px;
padding : 8px 5px 13px;
background-color : #333;
color : white;
text-decoration : none;

View File

@@ -123,8 +123,8 @@ const RecentItems = createClass({
if(!this.state.showDropdown) return null;
const makeItems = (brews)=>{
return _.map(brews, (brew)=>{
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
return _.map(brews, (brew, i)=>{
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>
</a>;

View File

@@ -6,7 +6,7 @@ const cx = require('classnames');
const moment = require('moment');
const request = require('superagent');
const googleDriveIcon = require('../../../googleDrive.png');
const googleDriveIcon = require('../../../../googleDrive.png');
const dedent = require('dedent-tabs').default;
const BrewItem = createClass({

View File

@@ -17,6 +17,8 @@
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
box-shadow : 0px 4px 5px 0px #333;
background-color : #cab2802e;
.text {
min-height : 54px;
h4{

View File

@@ -0,0 +1,160 @@
require('./listPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const moment = require('moment');
const BrewItem = require('./brewItem/brewItem.jsx');
const ListPage = createClass({
displayName : 'ListPage',
getDefaultProps : function() {
return {
brewCollection : [
{
title : '',
class : '',
brews : []
}
],
navItems : <></>
};
},
getInitialState : function() {
return {
sortType : 'alpha',
sortDir : 'asc',
filterString : ''
};
},
renderBrews : function(brews){
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
return _.map(brews, (brew, idx)=>{
return <BrewItem brew={brew} key={idx}/>;
});
},
sortBrewOrder : function(brew){
if(!brew.title){brew.title = 'No Title';}
const mapping = {
'alpha' : _.deburr(brew.title.toLowerCase()),
'created' : moment(brew.createdAt).format(),
'updated' : moment(brew.updatedAt).format(),
'views' : brew.views,
'latest' : moment(brew.lastViewed).format()
};
return mapping[this.state.sortType];
},
handleSortOptionChange : function(event){
this.setState({
sortType : event.target.value
});
},
handleSortDirChange : function(event){
this.setState({
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
});
},
renderSortOption : function(sortTitle, sortValue){
return <td>
<button
value={`${sortValue}`}
onClick={this.handleSortOptionChange}
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
>
{`${sortTitle}`}
</button>
</td>;
},
handleFilterTextChange : function(e){
this.setState({
filterString : e.target.value
});
return;
},
renderFilterOption : function(){
return <td>
<label>
<i className='fas fa-search'></i>
<input
type='search'
placeholder='search title/description'
onChange={this.handleFilterTextChange}
/>
</label>
</td>;
},
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>
</div>;
},
getSortedBrews : function(brews){
const testString = _.deburr(this.state.filterString).toLowerCase();
brews = _.filter(brews, (brew)=>{
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
(_.deburr(brew.description).toLowerCase().includes(testString));
});
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
},
renderBrewCollection : function(brewCollection){
return _.map(brewCollection, (brewGroup, idx)=>{
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
<h1>{brewGroup.title || 'No Title'}</h1>
{this.renderBrews(this.getSortedBrews(brewGroup.brews))}
</div>;
});
},
render : function(){
return <div className='listPage sitePage'>
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
{this.props.navItems}
<div className='content V3'>
<div className='phb'>
{this.renderSortOptions()}
{this.renderBrewCollection(this.props.brewCollection)}
</div>
</div>
</div>;
}
});
module.exports = ListPage;

View File

@@ -11,7 +11,7 @@
-webkit-column-gap : auto;
-moz-column-gap : auto;
}
.userPage{
.listPage{
.content{
overflow-y : scroll;
.phb{

View File

@@ -10,7 +10,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
@@ -434,7 +434,7 @@ const EditPage = createClass({
{this.renderGoogleDriveIcon()}
{this.renderSaveButton()}
<NewBrew />
<ReportIssue />
<HelpNavItem/>
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share

View File

@@ -7,8 +7,8 @@ const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
@@ -33,7 +33,7 @@ const ErrorPage = createClass({
<Nav.section>
<PatreonNavItem />
<IssueNavItem />
<HelpNavItem />
<RecentNavItem />
</Nav.section>
</Navbar>

View File

@@ -9,7 +9,7 @@ const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx');
@@ -59,7 +59,7 @@ const HomePage = createClass({
return <Navbar ver={this.props.ver}>
<Nav.section>
<NewBrewItem />
<IssueNavItem />
<HelpNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>

View File

@@ -0,0 +1,202 @@
# How to Convert a Legacy Document to v3
Here you will find a number of steps to guide you through converting a Legacy document into a Homebrewery v3 document.
**The first thing you'll want to do is switch the editor's rendering engine from `Legacy` to `v3`.** This will be the renderer we design features for moving forward.
There are some examples of Legacy code in the code pane if you need more context behind some of the changes.
**This document will evolve as users like yourself inform us of issues with it, or areas of conversion that it does not cover. _Please_ reach out if you have any suggestions for this document.**
## Simple Replacements
To make your life a little easier with this section, a text editor like [VSCode](https://code.visualstudio.com/) or Notepad will help a lot.
The following table describes Legacy and other document elements and their Homebrewery counterparts. A simple find/replace should get these in working order.
| Legacy / Other | Homebrewery |
|:----------------|:-----------------------------|
| `\pagebreak` | `\page` |
| `======` | `\page` |
| `\pagebreaknum` | `{{pageNumber,auto}}\n\page` |
| `@=====` | `{{pageNumber,auto}}\n\page` |
| `\columnbreak` | `\column` |
| `.phb` | `.page` |
## Classed or Styled Divs
Anything that relies on the following syntax can be changed to the new Homebrewery v3 curly brace syntax:
```
<div class="classTable wide">
...
</div>
```
:
The above example is equivalent to the following in v3 syntax.
```
{{classTable,wide
...
}}
```
:
Some examples of this include class tables (as shown above), descriptive blocks, notes, and spell lists.
\column
## Margins and Padding
Any manual margins and padding to push text down the page will likely need to be updated. Colons can be used on lines by themselves to push things down the page vertically if you'd rather not set pixel-perfect margins or padding.
## Notes
In Legacy, notes are denoted using markdown blockquote syntax. In Homebrewery v3, this is replaced by the curly brace syntax.
<!--
> ##### Catchy Title
> Useful Information
-->
{{note
##### Title
Information
}}
## Split Tables
Split tables also use the curly brace syntax, as the new renderer can handle style values separately from class names.
<!--
<div style='column-count:2'>
| d8 | Loot |
|:---:|:-----------:|
| 1 | 100gp |
| 2 | 200gp |
| 3 | 300gp |
| 4 | 400gp |
| d8 | Loot |
|:---:|:-----------:|
| 5 | 500gp |
| 6 | 600gp |
| 7 | 700gp |
| 8 | 1000gp |
</div>
-->
##### Typical Difficulty Classes
{{column-count:2
| Task Difficulty | DC |
|:----------------|:--:|
| Very easy | 5 |
| Easy | 10 |
| Medium | 15 |
| Task Difficulty | DC |
|:------------------|:--:|
| Hard | 20 |
| Very hard | 25 |
| Nearly impossible | 30 |
}}
## Blockquotes
Blockquotes are denoted by the `>` character at the beginning of the line. In Homebrewery's v3 renderer, they hold virtually no meaning and have no CSS styling. You are free to use blockquotes when styling your document or creating themes without needing to worry about your CSS affecting other parts of the document.
{{pageNumber,auto}}
\page
## Stat Blocks
There are pretty significant differences between stat blocks on the Legacy renderer and Homebrewery v3. This section contains a list of changes that will need to be made to update the stat block.
### Initial Changes
You will want to **remove all leading** `___` that started the stat block in Legacy, and replace that with `{{monster` before the stat block, and `}}` after it.
**If you want a frame** around the stat block, you can add `,frame` to the curly brace definition.
**If the stat block was wide**, make sure to add `,wide` to the curly brace definition.
### Blockquotes
The key difference is the lack of blockquotes. Legacy documents use the `>` symbol at the start of the line for each line in the stat block, and the v3 renderer does not. **You will want to remove all `>` characters at the beginning of all lines, and delete any leading spaces.**
### Lists
The basic characteristics and advanced characteristics sections are not list elements in Homebrewery. You will want to **remove all `-` or `*` characters from the beginning of lines.**
### Spacing
In order to have the correct spacing after removing the list elements, you will want to **add two colons between the name of each basic/advanced characteristic and its value.** _(see example in the code pane)_
Additionally, in the special traits and actions sections, you will want to add a colon at the beginning of each line that separates a trait/action from another, as seen below. **Any empty lines between special traits and actions should contain only a colon.** _(see example in the code pane)_
\column
{{margin-top:102px}}
<!--
### Legacy/Other Document Example:
___
> ## Centaur
> *Large Monstrosity, neutral good*
>___
> - **Armor Class** 12
> - **Hit Points** 45(6d10 + 12)
> - **Speed** 50ft.
>___
>|STR|DEX|CON|INT|WIS|CHA|
>|:---:|:---:|:---:|:---:|:---:|:---:|
>|18 (+4)|14 (+2)|14 (+2)|9 (-1)|13 (+1)|11 (+0)|
>___
> - **Skills** Athletics +6, Perception +3, Survival +3
> - **Senses** passive Perception 13
> - **Languages** Elvish, Sylvan
> - **Challenge** 2 (450 XP)
> ___
> ***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
>
> ***Second Thing*** More details.
>
> ### Actions
> ***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
>
> ***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
>
> ***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
>
> ***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
-->
### Homebrewery v3 Example:
{{monster
## Centaur
*Large monstrosity, neutral good*
___
**Armor Class** :: 12
**Hit Points** :: 45(6d10 + 12)
**Speed** :: 50ft.
___
| STR | DEX | CON | INT | WIS | CHA |
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|18 (+4)|14 (+2)|14 (+2)|9 (-1) |13 (+1)|11 (+0)|
___
**Skills** :: Athletics +6, Perception +3, Survival +3
**Senses** :: passive Perception 13
**Languages** :: Elvish, Sylvan
**Challenge** :: 2 (450 XP)
___
***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
:
***Second Thing*** More details.
### Actions
***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
:
***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
:
***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
:
***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
}}
{{pageNumber,auto}}

View File

@@ -1,4 +1,5 @@
# The Homebrewery
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
### Homebrew D&D made easy
@@ -57,17 +58,20 @@ The Homebrewery is licensed using the [MIT License](https://github.com/naturalcr
If you wish to sell or in some way gain profit for what you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
### More Resources
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources).
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
<div class='pageNumber'>1</div>
<div class='footnote'>PART 1 | FANCINESS</div>
<div style='position: absolute; top: 20px; right: 20px;'>
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'><img src='/assets/discord.png' style='height:30px'/></a>
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
</div>
\page

View File

@@ -2,7 +2,6 @@
.page #example + table td {
border:1px dashed #00000030;
}
.page {
padding-bottom : 1.1cm;
}
@@ -50,9 +49,9 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
\column
## New in V3.0.0
With the latest major update to *The Homebrewery* we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
Much of the syntax and styling has changed in V3. Code in one version may be broken in the other, and updating an older brew to V3 will require more than just a copy and paste. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
Scroll down to the next page for a brief summary of the changes and new features available in V3!
@@ -80,8 +79,15 @@ If you wish to sell or in some way gain profit for what's created on this site,
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
### More Homebrew Resources
Check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources).
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
{{position:absolute;top:20px;right:20px;width:auto
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
}}
\page
@@ -123,9 +129,7 @@ A blank line can be achieved with a run of one or more `:` alone on a line. More
Much nicer than `<br><br><br><br><br>`
### Definition Lists
V3 uses HTML *definition lists* to create "lists" with hanging indents.
**Senses** :: Here is some text that is long and overflows into a second line, creating a "hanging indent".
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
### Column Breaks
Column and page breaks with `\column` and `\page`.
@@ -153,9 +157,9 @@ These can be combined to span a cell across both columns and rows. Cells must ha
| 6A | 6B ^| 6C |
## Images
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*. Images can be included using Markdown-style images.
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*.
Using *Curly Injection* you can assign an id, classes, or specific inline CSS properties to the image.
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
![alt-text](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:100px,border:"2px solid",border-radius:10px}

View File

@@ -11,7 +11,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
@@ -291,7 +291,7 @@ const NewPage = createClass({
<Nav.section>
{this.renderSaveButton()}
{this.renderLocalPrintButton()}
<IssueNavItem />
<HelpNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>

View File

@@ -1,10 +1,9 @@
require('./userPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const moment = require('moment');
const ListPage = require('../basePages/listPage/listPage.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
@@ -12,16 +11,7 @@ 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 BrewItem = require('./brewItem/brewItem.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx');
// const brew = {
// title : 'SUPER Long title woah now',
// authors : []
// };
//const BREWS = _.times(25, ()=>{ return brew;});
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const UserPage = createClass({
displayName : 'UserPage',
@@ -32,160 +22,47 @@ const UserPage = createClass({
};
},
getInitialState : function() {
return {
sortType : 'alpha',
sortDir : 'asc',
filterString : ''
};
},
getUsernameWithS : function() {
if(this.props.username.endsWith('s'))
return `${this.props.username}'`;
return `${this.props.username}'s`;
},
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `'` : `'s`);
renderBrews : function(brews){
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
const sortedBrews = this.sortBrews(brews);
return _.map(sortedBrews, (brew, idx)=>{
return <BrewItem brew={brew} key={idx}/>;
});
},
sortBrewOrder : function(brew){
if(!brew.title){brew.title = 'No Title';}
const mapping = {
'alpha' : _.deburr(brew.title.toLowerCase()),
'created' : moment(brew.createdAt).format(),
'updated' : moment(brew.updatedAt).format(),
'views' : brew.views,
'latest' : moment(brew.lastViewed).format()
};
return mapping[this.state.sortType];
},
sortBrews : function(brews){
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
},
handleSortOptionChange : function(event){
this.setState({
sortType : event.target.value
});
},
handleSortDirChange : function(event){
this.setState({
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
});
},
renderSortOption : function(sortTitle, sortValue){
return <td>
<button
value={`${sortValue}`}
onClick={this.handleSortOptionChange}
className={`sortOption ${(this.state.sortType == sortValue ? 'active' : '')}`}
>
{`${sortTitle}`}
</button>
</td>;
},
handleFilterTextChange : function(e){
this.setState({
filterString : e.target.value
});
return;
},
renderFilterOption : function(){
return <td>
<label className='filterOption'>
<i className='fas fa-search'></i>
<input
type='search'
placeholder='search title/description'
onChange={this.handleFilterTextChange}
/>
</label>
</td>;
},
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>
</div>;
},
getSortedBrews : function(){
const testString = _.deburr(this.state.filterString).toLowerCase();
const brewCollection = this.state.filterString ? _.filter(this.props.brews, (brew)=>{
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
(_.deburr(brew.description).toLowerCase().includes(testString));
}) : this.props.brews;
return _.groupBy(brewCollection, (brew)=>{
const brews = _.groupBy(this.props.brews, (brew)=>{
return (brew.published ? 'published' : 'private');
});
const brewCollection = [
{
title : `${usernameWithS} published brews`,
class : 'published',
brews : brews.published
}
];
if(this.props.username == global.account?.username){
brewCollection.push(
{
title : `${usernameWithS} unpublished brews`,
class : 'unpublished',
brews : brews.private
}
);
}
return {
brewCollection : brewCollection
};
},
navItems : function() {
return <Navbar>
<Nav.section>
<NewBrew />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>;
},
render : function(){
const brews = this.getSortedBrews();
return <div className='userPage sitePage'>
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
<Navbar>
<Nav.section>
<NewBrew />
<ReportIssue />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>
<div className='content V3'>
<div className='phb'>
{this.renderSortOptions()}
<div className='published'>
<h1>{this.getUsernameWithS()} published brews</h1>
{this.renderBrews(brews.published)}
</div>
{this.props.username == global.account?.username &&
<div className='unpublished'>
<h1>{this.getUsernameWithS()} unpublished brews</h1>
{this.renderBrews(brews.private)}
</div>
}
</div>
</div>
</div>;
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()}></ListPage>;
}
});

2880
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.0.6",
"version": "3.0.7",
"engines": {
"node": "16.11.x"
},
@@ -33,44 +33,47 @@
"jest": {
"modulePaths": [
"mode_modules",
"shared"
"shared",
"server"
]
},
"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
},
"dependencies": {
"@babel/core": "^7.16.7",
"@babel/plugin-transform-runtime": "^7.16.7",
"@babel/preset-env": "^7.16.7",
"@babel/core": "^7.17.2",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"body-parser": "^1.19.1",
"classnames": "^2.3.1",
"codemirror": "^5.65.0",
"codemirror": "^5.65.1",
"cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.1",
"express": "^4.17.2",
"express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.1",
"express-static-gzip": "2.1.3",
"fs-extra": "10.0.0",
"googleapis": "92.0.0",
"googleapis": "95.0.0",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "4.0.9",
"marked": "4.0.12",
"marked-extended-tables": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.1",
"mongoose": "^6.1.5",
"nanoid": "3.1.30",
"mongoose": "^6.2.1",
"nanoid": "3.2.0",
"nconf": "^0.11.3",
"prop-types": "15.8.0",
"query-string": "7.1.0",
"query-string": "7.1.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-frame-component": "4.1.3",
@@ -80,8 +83,9 @@
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.6.0",
"eslint": "^8.8.0",
"eslint-plugin-react": "^7.28.0",
"jest": "^27.4.5"
"jest": "^27.5.1",
"supertest": "^6.2.2"
}
}

View File

@@ -24,6 +24,28 @@ const build = async ({ bundle, render, ssr })=>{
await fs.outputFile('./build/homebrew/bundle.css', css);
await fs.outputFile('./build/homebrew/bundle.js', bundle);
await fs.outputFile('./build/homebrew/ssr.js', ssr);
await fs.copy('./themes/fonts', './build/fonts');
await fs.copy('./themes/assets', './build/assets');
let src = './themes/5ePhbLegacy.style.less';
//Parse brew theme files
less.render(fs.readFileSync(src).toString(), {
compress : !isDev
}, function(e, output) {
fs.outputFile('./build/themes/5ePhbLegacy.style.css', output.css);
});
src = './themes/5ePhb.style.less';
less.render(fs.readFileSync(src).toString(), {
compress : !isDev
}, function(e, output) {
fs.outputFile('./build/themes/5ePhb.style.css', output.css);
});
// await less.render(lessCode, {
// compress : !dev,
// sourceMap : (dev ? {
// sourceMapFileInline: true,
// outputSourceFiles: true
// } : false),
// })
//compress files in production
if(!isDev){
@@ -132,6 +154,6 @@ fs.emptyDirSync('./build');
if(isDev){
livereload('./build');
watchFile('./server.js', {
watch : ['./client'] // Watch additional folders if you want
watch : ['./client', './server'] // Watch additional folders if you want
});
}

307
server.js
View File

@@ -1,303 +1,12 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
const _ = require('lodash');
const jwt = require('jwt-simple');
const express = require('express');
const yaml = require('js-yaml');
const app = express();
const homebrewApi = require('./server/homebrew.api.js');
const GoogleActions = require('./server/googleActions.js');
const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler');
const brewAccessTypes = ['edit', 'share', 'raw'];
//Get the brew object from the HB database or Google Drive
const getBrewFromId = asyncHandler(async (id, accessType)=>{
if(!brewAccessTypes.includes(accessType))
throw ('Invalid Access Type when getting brew');
let brew;
if(id.length > 12) {
const googleId = id.slice(0, -12);
id = id.slice(-12);
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
} else {
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
}
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
//Split brew.text into text and style
//unless the Access Type is RAW, in which case return immediately
if(accessType == 'raw') {
return brew;
}
splitTextStyleAndMetadata(brew);
return brew;
});
const sanitizeBrew = (brew, full=false)=>{
delete brew._id;
delete brew.__v;
if(full){
delete brew.editId;
}
return brew;
};
const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```metadata')) {
const index = brew.text.indexOf('```\n\n');
const metadataSection = brew.text.slice(12, index - 1);
const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']));
brew.text = brew.text.slice(index + 5);
}
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
_.defaults(brew, { 'renderer': 'legacy', 'theme': '5ePHB' });
};
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
process.chdir(__dirname);
//app.use(express.static(`${__dirname}/build`));
app.use(require('body-parser').json({ limit: '25mb' }));
app.use(require('cookie-parser')());
app.use(require('./server/forcessl.mw.js'));
const config = require('nconf')
.argv()
.env({ lowerCase: true })
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });
// DB
const DB = require('./server/db.js');
DB.connect(config);
const server = require('./server/app.js');
const config = require('./server/config.js');
//Account Middleware
app.use((req, res, next)=>{
if(req.cookies && req.cookies.nc_session){
try {
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
//console.log("Just loaded up JWT from cookie:");
//console.log(req.account);
} catch (e){}
}
req.config = {
google_client_id : config.get('google_client_id'),
google_client_secret : config.get('google_client_secret')
};
return next();
});
app.use(homebrewApi);
app.use(require('./server/admin.api.js'));
const HomebrewModel = require('./server/homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
const welcomeTextV3 = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
const faqText = require('fs').readFileSync('./faq.md', 'utf8');
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
//Robots.txt
app.get('/robots.txt', (req, res)=>{
return res.sendFile(`${__dirname}/robots.txt`);
});
//Home page
app.get('/', async (req, res, next)=>{
const brew = {
text : welcomeText
};
req.brew = brew;
return next();
});
//Home page v3
app.get('/v3_preview', async (req, res, next)=>{
const brew = {
text : welcomeTextV3,
renderer : 'V3'
};
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
//Changelog page
app.get('/changelog', async (req, res, next)=>{
const brew = {
title : 'Changelog',
text : changelogText,
renderer : 'V3'
};
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
//FAQ page
app.get('/faq', async (req, res, next)=>{
const brew = {
title : 'FAQ',
text : faqText,
renderer : 'V3'
};
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
//Source page
app.get('/source/:id', asyncHandler(async (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'raw');
const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text;
for (const replaceStr in replaceStrings) {
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
}
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
res.status(200).send(text);
}));
//Download brew source page
app.get('/download/:id', asyncHandler(async (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'raw');
const prefix = 'HB - ';
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/plain',
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
DB.connect(config).then(()=>{
// Ensure that we have successfully connected to the database
// before launching server
const PORT = process.env.PORT || config.get('web_port') || 8000;
server.app.listen(PORT, ()=>{
console.log(`server on port: ${PORT}`);
});
res.status(200).send(brew.text);
}));
//User Page
app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username);
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
.catch((err)=>{
console.log(err);
});
if(ownAccount && req?.account?.googleId){
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
.catch((err)=>{
console.error(err);
});
if(googleBrews)
brews = _.concat(brews, googleBrews);
}
req.brews = _.map(brews, (brew)=>{
return sanitizeBrew(brew, !ownAccount);
});
return next();
});
//Edit Page
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
const brew = await getBrewFromId(req.params.id, 'edit');
req.brew = brew;
return next();
}));
//New Page
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
brew.title = `CLONE - ${brew.title}`;
req.brew = brew;
return next();
}));
//Share Page
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
req.brew = brew;
return next();
}));
//Print Page
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
req.brew = brew;
return next();
}));
//Render the page
const templateFn = require('./client/template.js');
app.use((req, res)=>{
const props = {
version : require('./package.json').version,
url : req.originalUrl,
brew : req.brew,
brews : req.brews,
googleBrews : req.googleBrews,
account : req.account,
enable_v3 : config.get('enable_v3')
};
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
.then((page)=>{ res.send(page); })
.catch((err)=>{
console.log(err);
return res.sendStatus(500);
});
});
//v=====----- Error-Handling Middleware -----=====v//
//Format Errors so all fields will be sent
const replaceErrors = (key, value)=>{
if(value instanceof Error) {
const error = {};
Object.getOwnPropertyNames(value).forEach(function (key) {
error[key] = value[key];
});
return error;
}
return value;
};
const getPureError = (error)=>{
return JSON.parse(JSON.stringify(error, replaceErrors));
};
app.use((err, req, res, next)=>{
const status = err.status || 500;
console.error(err);
res.status(status).send(getPureError(err));
});
//^=====--------------------------------------=====^//
const PORT = process.env.PORT || config.get('web_port') || 8000;
app.listen(PORT, ()=>{
console.log(`server on port:${PORT}`);
});

306
server/app.js Normal file
View File

@@ -0,0 +1,306 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
// Set working directory to project root
process.chdir(`${__dirname}/..`);
const _ = require('lodash');
const jwt = require('jwt-simple');
const express = require('express');
const yaml = require('js-yaml');
const app = express();
const config = require('./config.js');
const homebrewApi = require('./homebrew.api.js');
const GoogleActions = require('./googleActions.js');
const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler');
const brewAccessTypes = ['edit', 'share', 'raw'];
//Get the brew object from the HB database or Google Drive
const getBrewFromId = asyncHandler(async (id, accessType)=>{
if(!brewAccessTypes.includes(accessType))
throw ('Invalid Access Type when getting brew');
let brew;
if(id.length > 12) {
const googleId = id.slice(0, -12);
id = id.slice(-12);
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
} else {
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
}
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
//Split brew.text into text and style
//unless the Access Type is RAW, in which case return immediately
if(accessType == 'raw') {
return brew;
}
splitTextStyleAndMetadata(brew);
return brew;
});
const sanitizeBrew = (brew, full=false)=>{
delete brew._id;
delete brew.__v;
if(full){
delete brew.editId;
}
return brew;
};
const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```metadata')) {
const index = brew.text.indexOf('```\n\n');
const metadataSection = brew.text.slice(12, index - 1);
const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
brew.text = brew.text.slice(index + 5);
}
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
};
app.use('/', serveCompressedStaticAssets(`build`));
//app.use(express.static(`${__dirname}/build`));
app.use(require('body-parser').json({ limit: '25mb' }));
app.use(require('cookie-parser')());
app.use(require('./forcessl.mw.js'));
//Account Middleware
app.use((req, res, next)=>{
if(req.cookies && req.cookies.nc_session){
try {
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
//console.log("Just loaded up JWT from cookie:");
//console.log(req.account);
} catch (e){}
}
req.config = {
google_client_id : config.get('google_client_id'),
google_client_secret : config.get('google_client_secret')
};
return next();
});
app.use(homebrewApi);
app.use(require('./admin.api.js'));
const HomebrewModel = require('./homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
const welcomeTextV3 = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
const faqText = require('fs').readFileSync('faq.md', 'utf8');
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
//Robots.txt
app.get('/robots.txt', (req, res)=>{
return res.sendFile(`robots.txt`, { root: process.cwd() });
});
//Home page
app.get('/', async (req, res, next)=>{
const brew = {
text : welcomeText
};
req.brew = brew;
return next();
});
//Home page v3
app.get('/v3_preview', async (req, res, next)=>{
const brew = {
text : welcomeTextV3,
renderer : 'V3'
};
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
//Legacy/Other Document -> v3 Migration Guide
app.get('/migrate', async (req, res, next)=>{
const brew = {
text : migrateText,
renderer : 'V3'
};
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
//Changelog page
app.get('/changelog', async (req, res, next)=>{
const brew = {
title : 'Changelog',
text : changelogText,
renderer : 'V3'
};
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
//FAQ page
app.get('/faq', async (req, res, next)=>{
const brew = {
title : 'FAQ',
text : faqText,
renderer : 'V3'
};
splitTextStyleAndMetadata(brew);
req.brew = brew;
return next();
});
//Source page
app.get('/source/:id', asyncHandler(async (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'raw');
const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text;
for (const replaceStr in replaceStrings) {
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
}
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
res.status(200).send(text);
}));
//Download brew source page
app.get('/download/:id', asyncHandler(async (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'raw');
const prefix = 'HB - ';
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/plain',
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
});
res.status(200).send(brew.text);
}));
//User Page
app.get('/user/:username', async (req, res, next)=>{
const ownAccount = req.account && (req.account.username == req.params.username);
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
.catch((err)=>{
console.log(err);
});
if(ownAccount && req?.account?.googleId){
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
.catch((err)=>{
console.error(err);
});
if(googleBrews)
brews = _.concat(brews, googleBrews);
}
req.brews = _.map(brews, (brew)=>{
return sanitizeBrew(brew, !ownAccount);
});
return next();
});
//Edit Page
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
const brew = await getBrewFromId(req.params.id, 'edit');
req.brew = brew;
return next();
}));
//New Page
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
brew.title = `CLONE - ${brew.title}`;
req.brew = brew;
return next();
}));
//Share Page
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
req.brew = brew;
return next();
}));
//Print Page
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
req.brew = brew;
return next();
}));
//Render the page
const templateFn = require('./../client/template.js');
app.use((req, res)=>{
const props = {
version : require('./../package.json').version,
url : req.originalUrl,
brew : req.brew,
brews : req.brews,
googleBrews : req.googleBrews,
account : req.account,
enable_v3 : config.get('enable_v3')
};
const title = req.brew ? req.brew.title : '';
templateFn('homebrew', title, props)
.then((page)=>{ res.send(page); })
.catch((err)=>{
console.log(err);
return res.sendStatus(500);
});
});
//v=====----- Error-Handling Middleware -----=====v//
//Format Errors so all fields will be sent
const replaceErrors = (key, value)=>{
if(value instanceof Error) {
const error = {};
Object.getOwnPropertyNames(value).forEach(function (key) {
error[key] = value[key];
});
return error;
}
return value;
};
const getPureError = (error)=>{
return JSON.parse(JSON.stringify(error, replaceErrors));
};
app.use((err, req, res, next)=>{
const status = err.status || 500;
console.error(err);
res.status(status).send(getPureError(err));
});
//^=====--------------------------------------=====^//
module.exports = {
app : app
};

5
server/config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = require('nconf')
.argv()
.env({ lowerCase: true })
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });

View File

@@ -3,15 +3,11 @@ const _ = require('lodash');
const { google } = require('googleapis');
const { nanoid } = require('nanoid');
const token = require('./token.js');
const config = require('nconf')
.argv()
.env({ lowerCase: true }) // Load environment variables
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });
const config = require('./config.js');
//let oAuth2Client;
GoogleActions = {
const GoogleActions = {
authCheck : (account, res)=>{
if(!account || !account.googleId){ // If not signed into Google

View File

@@ -1,11 +1,7 @@
const jwt = require('jwt-simple');
// Load configuration values
const config = require('nconf')
.argv()
.env({ lowerCase: true }) // Load environment variables
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });
const config = require('./config.js');
// Generate an Access Token for the given User ID
const generateAccessToken = (account)=>{

View File

@@ -1,3 +1,4 @@
@import '../styles/colors';
@keyframes glideDropDown {
0% {transform : translate(0px, -100%);
opacity : 0;
@@ -49,6 +50,7 @@ nav{
}
}
.navItem{
#backgroundColors;
.animate(background-color);
padding : 8px 12px;
cursor : pointer;
@@ -58,30 +60,12 @@ nav{
color : white;
text-decoration : none;
text-transform : uppercase;
line-height : 13px;
i{
margin-left : 5px;
font-size : 13px;
float : right;
}
&.tealLight:hover{ background-color : @tealLight };
&.teal:hover{ background-color : @teal };
&.greenLight:hover{ background-color : @greenLight };
&.green:hover{ background-color : @green };
&.blueLight:hover{ background-color : @blueLight };
&.blue:hover{ background-color : @blue };
&.purpleLight:hover{ background-color : @purpleLight };
&.purple:hover{ background-color : @purple };
&.steelLight:hover{ background-color : @steelLight };
&.steel:hover{ background-color : @steel };
&.yellowLight:hover{ background-color : @yellowLight };
&.yellow:hover{ background-color : @yellow };
&.orangeLight:hover{ background-color : @orangeLight };
&.orange:hover{ background-color : @orange };
&.redLight:hover{ background-color : @redLight };
&.red:hover{ background-color : @red };
&.silverLight:hover{ background-color : @silverLight };
&.silver:hover{ background-color : @silver };
&.greyLight:hover{ background-color : @greyLight };
&.grey:hover{ background-color : @grey };
}
.navSection:last-child .navItem{
border-left : 1px solid #666;

View File

@@ -20,4 +20,27 @@
@silverLight : #ECF0F1;
@silver : #BDC3C7;
@greyLight : #95A5A6;
@grey : #7F8C8D;
@grey : #7F8C8D;
#backgroundColors {
&.tealLight:hover{ background-color : @tealLight };
&.teal:hover{ background-color : @teal };
&.greenLight:hover{ background-color : @greenLight };
&.green:hover{ background-color : @green };
&.blueLight:hover{ background-color : @blueLight };
&.blue:hover{ background-color : @blue };
&.purpleLight:hover{ background-color : @purpleLight };
&.purple:hover{ background-color : @purple };
&.steelLight:hover{ background-color : @steelLight };
&.steel:hover{ background-color : @steel };
&.yellowLight:hover{ background-color : @yellowLight };
&.yellow:hover{ background-color : @yellow };
&.orangeLight:hover{ background-color : @orangeLight };
&.orange:hover{ background-color : @orange };
&.redLight:hover{ background-color : @redLight };
&.red:hover{ background-color : @red };
&.silverLight:hover{ background-color : @silverLight };
&.silver:hover{ background-color : @silver };
&.greyLight:hover{ background-color : @greyLight };
&.grey:hover{ background-color : @grey };
}

View File

@@ -0,0 +1,27 @@
const supertest = require('supertest');
// Mimic https responses to avoid being redirected all the time
const app = supertest.agent(require('app.js').app)
.set('X-Forwarded-Proto', 'https');
describe('Tests for static pages', ()=>{
it('Home page works', ()=>{
return app.get('/').expect(200);
});
it('Home page v3 works', ()=>{
return app.get('/v3_preview').expect(200);
});
it('Changelog page works', ()=>{
return app.get('/changelog').expect(200);
});
it('FAQ page works', ()=>{
return app.get('/faq').expect(200);
});
it('robots.txt works', ()=>{
return app.get('/robots.txt').expect(200);
});
});

BIN
themes/assets/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_3" data-name="Layer 3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350.028 390.552"><defs><style>.cls-1{opacity:0.9;}.cls-2{fill:#001013;}.cls-3{fill:#fff;}.cls-4{fill:#aa2a29;}</style></defs><title>Discord of Many Things blank</title><g class="cls-1"><path d="M100.991,82.743c-1.649,3.324-3.395,5.768-4.1,8.482-1.49,5.753-3.44,11.644-3.455,17.482q-.332,125.259-.141,250.519c.007,20.122,12.708,36.368,31.407,40.239a38.328,38.328,0,0,0,7.722.761q103.611,0,207.222-.088h6.314c-3.256-11.462-9.664-34.015-9.664-34.015l2.431,2.023s41.561,37.755,62.33,56.6c2.489,2.26,3.694,4.462,3.593,7.885-.241,8.186-.078,16.383-.078,25.492-6.333-5.527-11.928-10.35-17.457-15.247-9.586-8.493-19.2-16.957-28.644-25.6a10.032,10.032,0,0,0-7.39-2.774q-117.228-.1-234.457-.334c-20.632-.031-33.959-9.189-41.021-28.578-.852-2.339-.783-5.091-.784-7.653q-.057-116.5-.031-233.007V119.145C74.791,102.517,84.2,89.068,100.991,82.743Z" transform="translate(-74.79 -67.573)"/></g><path class="cls-2" d="M338.234,371.589s5.556,19.924,8.513,30.53h-5.075c-69.335,0-138.67-.1-208,.074-16.925.043-35.116-9.515-40.505-33.247a26.538,26.538,0,0,1-.379-5.827q-.027-128.172.025-256.344c.021-19.276,15.226-36.749,34.427-38.688a82.669,82.669,0,0,1,8.279-.447q123.288-.034,246.578-.067c13.265-.016,24.7,3.79,33.278,14.235a39.273,39.273,0,0,1,9.447,25.5q-.015,167.477-.012,334.955c0,1.245-.1,2.49-.2,4.645-4.075-3.565-7.6-6.552-11.018-9.655q-35.236-31.992-70.44-64.017c-1.2-1.091-3.713-3.147-3.713-3.147l-2-1.775Z" transform="translate(-74.79 -67.573)"/><path class="cls-3" d="M419.838,435.934c-7.848-6.892-15.026-12.977-21.961-19.329q-28.235-25.86-56.253-51.954c-2.314-2.157-4.59-3.247-7.356-1.784-3.036,1.607-2.77,4.676-2.022,7.359,2.286,8.2,4.817,16.338,7.24,24.5a17.108,17.108,0,0,1,.294,1.851c-1.486.084-2.872.233-4.258.233-67.363.012-134.726-.155-202.088.123-18.267.075-32.955-13.095-34.769-29.353a92.257,92.257,0,0,1-.886-10.108Q97.7,233.488,97.793,109.5c.013-10.518,3.19-20.063,11.156-27.471a31.92,31.92,0,0,1,20.439-8.917c1.948-.109,3.9-.181,5.852-.181q123.5-.014,247-.063c9.957-.012,18.894,2.145,26.329,9.1,7.956,7.441,11.177,16.934,11.214,27.457.142,40.84.057,81.681.057,122.522V435.934Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M311.029,170.975c-11.8,45.005-23.315,88.959-35.123,134.016l-97.168-98.883Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M380.8,265.712l-95.97,40.316c11.438-43.808,22.639-86.709,33.839-129.609l1.08-.272Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.3,322.955c2.91-36.463,5.761-72.179,8.709-109.116l94.711,96.472Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M223.458,107.926l83.127,55.063L178.71,196.919Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M366.285,281.366l-72.5,73.414c-3.524-13.182-6.849-25.617-10.306-38.549Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M274.874,318.478c3.445,12.8,6.765,25.143,10.376,38.559l-98.635-26.556.032-1.153Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M374.888,241.261l-51.71-75.57,26.8-21.529,25.831,96.619Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.492,207.523c-2.39,30.748-4.736,60.942-7.083,91.135l-1.063.159c-8.5-31.936-17-63.872-25.705-96.581Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M317.039,159.316l-68.251-45.24.307-.794,93.794,25.212Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M136.517,193.952,203.5,125.03l1.086.832L167.794,198.9Z" transform="translate(-74.79 -67.573)"/></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
themes/assets/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
themes/assets/patreon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
themes/assets/reddit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB