mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-23 14:23:21 +00:00
Compare commits
124 Commits
v3.16.1
...
Add-missin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fde21868cd | ||
|
|
980a7bd57e | ||
|
|
6b0022ad00 | ||
|
|
0f33973e58 | ||
|
|
7a41a140fd | ||
|
|
57467701d0 | ||
|
|
9dbfb26e6c | ||
|
|
7a169cbd9e | ||
|
|
2dc8a8fbe9 | ||
|
|
5f14f656ef | ||
|
|
6e8a0d7314 | ||
|
|
e61144beb8 | ||
|
|
64b792c645 | ||
|
|
aee5b7a8cc | ||
|
|
0148eafce0 | ||
|
|
a3ec5b8d3b | ||
|
|
4ded48df1e | ||
|
|
bc14246fe7 | ||
|
|
fcf985a115 | ||
|
|
a060fd123c | ||
|
|
7c7e143365 | ||
|
|
efa8f3fedf | ||
|
|
972a93d292 | ||
|
|
35be1e9b94 | ||
|
|
1a91c390f8 | ||
|
|
206e4fbda8 | ||
|
|
af98cb3867 | ||
|
|
f8fc6f7aa4 | ||
|
|
eb0fa28a03 | ||
|
|
4ab1a22eb3 | ||
|
|
962a46a670 | ||
|
|
cb16b32016 | ||
|
|
56f348f7ed | ||
|
|
b7c99b2d52 | ||
|
|
889f80f537 | ||
|
|
c270a69bb9 | ||
|
|
db0df82202 | ||
|
|
1346361f80 | ||
|
|
fdaf9d4808 | ||
|
|
3cdfae4270 | ||
|
|
a9275698fa | ||
|
|
99f2972079 | ||
|
|
afc92c4545 | ||
|
|
b26526a2f1 | ||
|
|
4f57f006ce | ||
|
|
666a94cd65 | ||
|
|
f0c094e9d8 | ||
|
|
a1c228b1d1 | ||
|
|
5e5c637c79 | ||
|
|
d573129f31 | ||
|
|
57cb334c15 | ||
|
|
c29e1905bf | ||
|
|
52d00b17a4 | ||
|
|
35364c400a | ||
|
|
77f0c1bf56 | ||
|
|
2d281072fa | ||
|
|
83b8f9c3b7 | ||
|
|
3a20452214 | ||
|
|
3e4ba89ed9 | ||
|
|
2c5c3d40df | ||
|
|
213240327d | ||
|
|
eca0f59b40 | ||
|
|
51936a1b99 | ||
|
|
6136b78395 | ||
|
|
81f56ec91d | ||
|
|
c7d94b0779 | ||
|
|
9758797e2b | ||
|
|
74a7983757 | ||
|
|
4eb8abf1e7 | ||
|
|
23910cc94c | ||
|
|
ef0ee78758 | ||
|
|
1b20c00842 | ||
|
|
db9212bd12 | ||
|
|
7348ecbb3d | ||
|
|
31a22703c1 | ||
|
|
33f8f6bf38 | ||
|
|
a62d2bd457 | ||
|
|
ffa9666bb9 | ||
|
|
406f5d4e14 | ||
|
|
ed404d3906 | ||
|
|
3178c8722e | ||
|
|
14a0f79ac8 | ||
|
|
136a6d4024 | ||
|
|
737e27f062 | ||
|
|
ee9143fa35 | ||
|
|
c62bb53660 | ||
|
|
4e5a971f0a | ||
|
|
16184f1b8d | ||
|
|
23bd0309b9 | ||
|
|
20dba6b7b3 | ||
|
|
5177c9a64e | ||
|
|
d179c18c35 | ||
|
|
6e4e35c7ad | ||
|
|
6c2721d49f | ||
|
|
029d61b6ad | ||
|
|
f58040e9a4 | ||
|
|
9f9948f531 | ||
|
|
2743ab869a | ||
|
|
4b21538e3e | ||
|
|
e17db0788c | ||
|
|
bea74c3b46 | ||
|
|
e252a39bd2 | ||
|
|
7ef259ddbe | ||
|
|
d18005fad4 | ||
|
|
86402cdbc8 | ||
|
|
e28b4e8c20 | ||
|
|
7c09680939 | ||
|
|
3f0a6a577f | ||
|
|
6f4cc0d91b | ||
|
|
e711a1c207 | ||
|
|
7a76c67038 | ||
|
|
108d368d45 | ||
|
|
bd413cfc55 | ||
|
|
1af13b4e94 | ||
|
|
2abc2b13f0 | ||
|
|
35856ad01e | ||
|
|
766fd40b72 | ||
|
|
3e6884b506 | ||
|
|
2118142faa | ||
|
|
2b270ccdb7 | ||
|
|
08eabf8102 | ||
|
|
c1d85bc216 | ||
|
|
3a2c213cf8 | ||
|
|
99dc0deb08 |
@@ -73,6 +73,9 @@ jobs:
|
||||
- run:
|
||||
name: Test - Variables
|
||||
command: npm run test:variables
|
||||
- run:
|
||||
name: Test - Emojis
|
||||
command: npm run test:emojis
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
@@ -82,6 +85,9 @@ jobs:
|
||||
- run:
|
||||
name: Test - Coverage
|
||||
command: npm run test:coverage
|
||||
- run:
|
||||
name: Test - Content Negotiation
|
||||
command: npm run test:content-negotiation
|
||||
|
||||
workflows:
|
||||
build_and_test:
|
||||
|
||||
25
.github/pull_request_template.md
vendored
25
.github/pull_request_template.md
vendored
@@ -1,26 +1,29 @@
|
||||
<!--
|
||||
Before submitting a Pull Request, please consider the following to speed up reviews:
|
||||
- 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
||||
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
||||
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
||||
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||
-->
|
||||
> [!TIP]
|
||||
> Before submitting a Pull Request, please consider the following to speed up reviews:
|
||||
> - 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
||||
> - 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
||||
> - 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
||||
> - 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
||||
|
||||
## Description
|
||||
|
||||
_Describe what your PR accomplishes. Consider walking through the main changes to aid reviewers in following your code, especially if it covers multiple files._
|
||||
|
||||
## Related Issues or Discussions
|
||||
|
||||
> [!CAUTION]
|
||||
> If no issue exists yet, create it, and get agreement on the approach (or paste in a previous agreement from chat, etc.) before moving forward. (Experimental PRs are OK without prior discussion, but do not expect to get merged.)
|
||||
|
||||
- Closes #
|
||||
|
||||
## QA Instructions, Screenshots, Recordings
|
||||
|
||||
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||
images for UI changes._
|
||||
_Replace this line with instructions on how to test or view your changes, as well as any before/after
|
||||
screenshots or recordings for UI changes._
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
_Please replace the list below with specific features you want reviewers to look at._
|
||||
_Replace the list below with specific features you want reviewers to look at._
|
||||
|
||||
*Reviewers, refer to this list when testing features, or suggest new items *
|
||||
- [ ] Verify new features are functional
|
||||
@@ -32,5 +35,3 @@ _Please replace the list below with specific features you want reviewers to look
|
||||
- [ ] Feature A handles negative numbers
|
||||
- [ ] Identify opportunities for simplification and refactoring
|
||||
- [ ] Check for code legibility and appropriate comments
|
||||
|
||||
<details><summary>Copy this list</summary>
|
||||
|
||||
@@ -217,7 +217,7 @@ const BrewRenderer = (props)=>{
|
||||
}
|
||||
|
||||
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
||||
renderedPages = useMemo(()=>renderPages(), [props.text]);
|
||||
renderedPages = useMemo(()=>renderPages(), [displayOptions.pageShadows, props.text]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -115,10 +115,10 @@
|
||||
color : #D3D3D3;
|
||||
accent-color : #D3D3D3;
|
||||
|
||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
||||
&::-webkit-slider-thumb, &::-moz-range-thumb {
|
||||
width : 5px;
|
||||
height : 5px;
|
||||
cursor : pointer;
|
||||
cursor : ew-resize;
|
||||
outline : none;
|
||||
}
|
||||
|
||||
|
||||
@@ -207,19 +207,11 @@ const Snippetbar = createClass({
|
||||
renderEditorButtons : function(){
|
||||
if(!this.props.showEditButtons) return;
|
||||
|
||||
const foldButtons = <>
|
||||
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
|
||||
onClick={this.props.foldCode} >
|
||||
<i className='fas fa-compress-alt' />
|
||||
</div>
|
||||
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
|
||||
onClick={this.props.unfoldCode} >
|
||||
<i className='fas fa-expand-alt' />
|
||||
</div>
|
||||
</>;
|
||||
|
||||
|
||||
return <div className='editors'>
|
||||
<div className='historyTools'>
|
||||
return (
|
||||
<div className='editors'>
|
||||
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||
onClick={this.toggleHistoryMenu} >
|
||||
<i className='fas fa-clock-rotate-left' />
|
||||
@@ -235,14 +227,21 @@ const Snippetbar = createClass({
|
||||
</div>
|
||||
</div>
|
||||
<div className='codeTools'>
|
||||
{foldButtons}
|
||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||
onClick={this.props.foldCode} >
|
||||
<i className='fas fa-compress-alt' />
|
||||
</div>
|
||||
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||
onClick={this.props.unfoldCode} >
|
||||
<i className='fas fa-expand-alt' />
|
||||
</div>
|
||||
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||
onClick={this.toggleThemeSelector} >
|
||||
<i className='fas fa-palette' />
|
||||
{this.state.themeSelector && this.renderThemeSelector()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div></>}
|
||||
|
||||
|
||||
<div className='tabs'>
|
||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||
@@ -259,7 +258,8 @@ const Snippetbar = createClass({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
render : function(){
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
justify-content : flex-end;
|
||||
min-width : 225px;
|
||||
|
||||
&:only-child { margin-left : auto; }
|
||||
&:only-child { margin-left : auto;min-width:unset;}
|
||||
|
||||
>div {
|
||||
display : flex;
|
||||
@@ -38,6 +38,11 @@
|
||||
line-height : @menuHeight;
|
||||
text-align : center;
|
||||
cursor : pointer;
|
||||
|
||||
&.editorTool:not(.active) {
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
&:hover,&.selected { background-color : #999999; }
|
||||
&.text {
|
||||
.tooltipLeft('Brew Editor');
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
require('./homebrew.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const { StaticRouter:Router } = require('react-router-dom/server');
|
||||
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
|
||||
const { StaticRouter:Router } = require('react-router');
|
||||
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
||||
|
||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||
|
||||
@@ -16,6 +16,7 @@ const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
@@ -380,7 +381,7 @@ const EditPage = createClass({
|
||||
|
||||
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
||||
|
||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
@@ -417,6 +418,7 @@ const EditPage = createClass({
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
<PrintNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
|
||||
@@ -8,6 +8,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
@@ -110,6 +111,7 @@ const SharePage = createClass({
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
</>}
|
||||
<VaultNavItem/>
|
||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
|
||||
@@ -92,49 +92,11 @@
|
||||
|
||||
&:invalid { background : rgb(255, 188, 181); }
|
||||
|
||||
&[type='checkbox'] {
|
||||
position : relative;
|
||||
display : inline-block;
|
||||
width : 50px;
|
||||
height : 30px;
|
||||
font-family : 'WalterTurncoat';
|
||||
font-size : 20px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
letter-spacing : 2px;
|
||||
appearance : none;
|
||||
background : red;
|
||||
isolation : isolate;
|
||||
border-radius : 5px;
|
||||
|
||||
&::before,&::after {
|
||||
position : absolute;
|
||||
inset : 0;
|
||||
z-index : 5;
|
||||
padding-top : 2px;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display : block;
|
||||
content : 'No';
|
||||
}
|
||||
|
||||
&::after {
|
||||
display : none;
|
||||
content : 'Yes';
|
||||
}
|
||||
|
||||
&:checked {
|
||||
background : green;
|
||||
|
||||
&::before { display : none; }
|
||||
&::after { display : block; }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#searchButton {
|
||||
.colorButton(@green);
|
||||
position : absolute;
|
||||
right : 20px;
|
||||
bottom : 0;
|
||||
@@ -152,7 +114,6 @@
|
||||
flex-direction : column;
|
||||
height : 100%;
|
||||
overflow-y : auto;
|
||||
font-family : 'BookInsanityRemake';
|
||||
font-size : 0.34cm;
|
||||
|
||||
h3 {
|
||||
@@ -356,6 +317,7 @@
|
||||
}
|
||||
|
||||
button {
|
||||
.colorButton(@green);
|
||||
width : max-content;
|
||||
|
||||
&.previousPage { grid-area : previousPage; }
|
||||
|
||||
19
faq.md
19
faq.md
@@ -69,7 +69,6 @@ pre {
|
||||
|
||||
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
||||
|
||||
|
||||
### Why am I getting an error when trying to save, and my account is linked to Google?
|
||||
|
||||
A sign-in with Google only lasts a year until the authentication expires. You must go [here](https://www.naturalcrit.com/login), click the *Log-out* button, and then sign back in using your Google account.
|
||||
@@ -82,12 +81,17 @@ If you have linked your account with a Google account, you would change your pas
|
||||
|
||||
### Is there a way to restore a previous version of my brew?
|
||||
|
||||
Currently, there is no way to do this through the site yourself. This would take too much of a toll on the amount of storage the homebrewery requires. However, we do have daily backups of our database that we keep for 8 days, and you can contact the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, the name of the lost brew, and the last known time it was working properly. We can manually look through our backups and restore it if it exists.
|
||||
In your brew, there is an icon, :fas_clock_rotate_left:, that button opens up a menu with versions of your brew, stored in order from newer to older, up to a week old. Because of the amount of duplicates this function creates, this information is stored in **your browser**, so if you were to uninstall it or clear your cookies and site data, or change computers, the info will not be there.
|
||||
|
||||
Also, we do have daily backups of our database that we keep for 8 days, and you can contact the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, the name of the lost brew, and the last known time it was working properly. We can manually look through our backups and restore it if it exists.
|
||||
|
||||
|
||||
### I worked on a brew for X hours, and suddenly all the text disappeared!
|
||||
|
||||
This usually happens if you accidentally drag-select all of your text and then start typing which overwrites the selection. Do not panic, and do not refresh the page or reload your brew quite yet as it is probably auto-saved in this state already. Simply press CTRL+Z as many times as needed to undo your last few changes and you will be back to where you were, then make sure to save your brew in the "good" state.
|
||||
|
||||
You can also load a history version old enough to have all the text, using the :fas_clock_rotate_left: history versions button.
|
||||
|
||||
\column
|
||||
|
||||
### Why is only Chrome supported?
|
||||
@@ -112,10 +116,7 @@ Once you have an image you would like to use, it is recommended to host it somew
|
||||
\page
|
||||
|
||||
### A particular font does not work for my language, what do I do?
|
||||
The fonts used were originally created for use with the English language, though revisions since then have added more support for other languages. They are still not complete sets and may be missing a glyph/character you need. Unfortunately, the volunteer group as it stands at the time of this writing does not have a font guru, so it would be difficult to add more glyphs (especially complicated glyphs). Let us know which glyph is missing on the subreddit, but you may need to search [Google Fonts](https://fonts.google.com) for an alternative font if you need something fast.
|
||||
|
||||
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
||||
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
||||
The fonts used were originally created for use with the English language, though revisions since then have added more support for other languages. They are still not complete sets and may be missing a glyph/character you need. Unfortunately, the volunteer group as it stands at the time of this writing does not have a font guru, so it would be difficult to add more glyphs (especially complicated glyphs). Let us know which glyph is missing on the subreddit, but you may need to search [Google Fonts](https://fonts.google.com) for an alternative font if you need something fast.
|
||||
|
||||
### I have white borders on the bottom/sides of the print preview.
|
||||
|
||||
@@ -126,4 +127,8 @@ The Homebrewery defaults to creating US Letter page sizes. If you are printing
|
||||
|
||||
### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page?
|
||||
|
||||
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software.
|
||||
Your ad-blocking software is mistakenly assuming your text to be an ad. We recommend whitelisting homebrewery.naturalcrit.com in your ad-blocking software, as we have no ads.
|
||||
|
||||
### My username appears as _hidden_ when checking my brews in the Vault, why is that?
|
||||
|
||||
Your username is most likely your e-mail adress, and our code is picking that up and protecting your identity. This will remain as is, but you can ask for a name change by contacting the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, and your desired new name. You will also be asked to provide details about some of your unpublished brews, to verify your identity. No information will be leaked or shared.
|
||||
|
||||
1271
package-lock.json
generated
1271
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@@ -27,6 +27,7 @@
|
||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
||||
"test:content-negotiation": "jest \"server/middleware/.*.spec.js\" --verbose",
|
||||
"test:coverage": "jest --coverage --silent --runInBand",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
@@ -85,17 +86,18 @@
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/preset-react": "^7.25.9",
|
||||
"@babel/preset-react": "^7.26.3",
|
||||
"@googleapis/drive": "^8.14.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"classnames": "^2.5.1",
|
||||
"codemirror": "^5.65.6",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.3",
|
||||
"dompurify": "^3.2.1",
|
||||
"dompurify": "^3.2.3",
|
||||
"expr-eval": "^2.0.2",
|
||||
"express": "^4.21.1",
|
||||
"express": "^4.21.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.2.0",
|
||||
"fs-extra": "11.2.0",
|
||||
@@ -111,13 +113,13 @@
|
||||
"marked-smartypants-lite": "^1.0.2",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.30.1",
|
||||
"mongoose": "^8.8.2",
|
||||
"nanoid": "5.0.8",
|
||||
"mongoose": "^8.9.1",
|
||||
"nanoid": "5.0.9",
|
||||
"nconf": "^0.12.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^4.1.3",
|
||||
"react-router-dom": "6.28.0",
|
||||
"react-router": "^7.0.2",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"superagent": "^10.1.1",
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
@@ -125,15 +127,15 @@
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-jest": "^28.9.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"globals": "^15.12.0",
|
||||
"globals": "^15.14.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^16.10.0",
|
||||
"stylelint": "^16.12.0",
|
||||
"stylelint-config-recess-order": "^5.1.1",
|
||||
"stylelint-config-recommended": "^14.0.1",
|
||||
"supertest": "^7.0.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {model as HomebrewModel } from './homebrew.model.js';
|
||||
import {model as NotificationModel } from './notifications.model.js';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
import { model as NotificationModel } from './notifications.model.js';
|
||||
import express from 'express';
|
||||
import Moment from 'moment';
|
||||
import zlib from 'zlib';
|
||||
@@ -108,6 +108,9 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin',
|
||||
|
||||
req.body = brew;
|
||||
|
||||
// Remove Account from request to prevent Admin user from being added to brew as an Author
|
||||
req.account = undefined;
|
||||
|
||||
return await HomebrewAPI.updateBrew(req, res);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Set working directory to project root
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import packageJSON from './../package.json' with { type: "json" };
|
||||
import packageJSON from './../package.json' with { type: 'json' };
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
process.chdir(`${__dirname}/..`);
|
||||
@@ -26,7 +26,7 @@ import serveCompressedStaticAssets from './static-assets.mv.js';
|
||||
import sanitizeFilename from 'sanitize-filename';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import templateFn from '../client/template.js';
|
||||
import {model as HomebrewModel } from './homebrew.model.js';
|
||||
import { model as HomebrewModel } from './homebrew.model.js';
|
||||
|
||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
@@ -47,7 +47,7 @@ const sanitizeBrew = (brew, accessType)=>{
|
||||
return brew;
|
||||
};
|
||||
|
||||
app.set('trust proxy', 1 /* number of proxies between user and server */)
|
||||
app.set('trust proxy', 1 /* number of proxies between user and server */);
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`build`));
|
||||
app.use(contentNegotiation);
|
||||
@@ -55,6 +55,40 @@ app.use(bodyParser.json({ limit: '25mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(forceSSL);
|
||||
|
||||
import cors from 'cors';
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
|
||||
const corsOptions = {
|
||||
origin : (origin, callback)=>{
|
||||
|
||||
const allowedOrigins = [
|
||||
'https://homebrewery.naturalcrit.com',
|
||||
'https://www.naturalcrit.com',
|
||||
'https://naturalcrit-stage.herokuapp.com',
|
||||
'https://homebrewery-stage.herokuapp.com',
|
||||
];
|
||||
|
||||
if(isLocalEnvironment) {
|
||||
allowedOrigins.push('http://localhost:8000', 'http://localhost:8010');
|
||||
}
|
||||
|
||||
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
||||
|
||||
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
console.log(origin, 'not allowed');
|
||||
callback(new Error('Not allowed by CORS, if you think this is an error, please contact us'));
|
||||
}
|
||||
},
|
||||
methods : ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
credentials : true,
|
||||
};
|
||||
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
@@ -62,7 +96,9 @@ app.use((req, res, next)=>{
|
||||
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){}
|
||||
} catch (e){
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
req.config = {
|
||||
@@ -273,7 +309,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
brews.forEach(brew => brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||
brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const auth = await GoogleActions.authCheck(req.account, res);
|
||||
@@ -312,6 +348,34 @@ app.get('/user/:username', async (req, res, next)=>{
|
||||
return next();
|
||||
});
|
||||
|
||||
//Change author name on brews
|
||||
app.put('/api/user/rename', async (req, res)=>{
|
||||
const { username, newUsername } = req.body;
|
||||
const ownAccount = req.account && (req.account.username == newUsername);
|
||||
|
||||
if(!username || !newUsername)
|
||||
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||
if(!ownAccount)
|
||||
return res.status(403).json({ error: 'Must be logged in to change your username' });
|
||||
try {
|
||||
const brews = await HomebrewModel.getByUser(username, true, ['authors']);
|
||||
const renamePromises = brews.map(async (brew)=>{
|
||||
const updatedAuthors = brew.authors.map((author)=>author === username ? newUsername : author
|
||||
);
|
||||
return HomebrewModel.updateOne(
|
||||
{ _id: brew._id },
|
||||
{ $set: { authors: updatedAuthors } }
|
||||
);
|
||||
});
|
||||
await Promise.all(renamePromises);
|
||||
|
||||
return res.json({ success: true, message: `Brews for ${username} renamed to ${newUsername}.` });
|
||||
} catch (error) {
|
||||
console.error('Error renaming brews:', error);
|
||||
return res.status(500).json({ error: 'Failed to rename brews.' });
|
||||
}
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||
@@ -399,7 +463,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
const data = {};
|
||||
data.title = 'Account Information Page';
|
||||
|
||||
|
||||
if(!req.account) {
|
||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||
const error = new Error('No valid account');
|
||||
@@ -413,7 +477,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
let googleCount = [];
|
||||
if(req.account) {
|
||||
if(req.account.googleId) {
|
||||
auth = await GoogleActions.authCheck(req.account, res, false)
|
||||
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||
|
||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
@@ -448,8 +512,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||
return next();
|
||||
}));
|
||||
|
||||
const nodeEnv = config.get('node_env');
|
||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||
// Local only
|
||||
if(isLocalEnvironment){
|
||||
// Login
|
||||
@@ -477,8 +539,8 @@ app.get('/vault', asyncHandler(async(req, res, next)=>{
|
||||
|
||||
//Send rendered page
|
||||
app.use(asyncHandler(async (req, res, next)=>{
|
||||
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||
|
||||
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||
|
||||
const page = await renderPage(req, res);
|
||||
if(!page) return;
|
||||
res.send(page);
|
||||
|
||||
@@ -241,8 +241,8 @@ const GoogleActions = {
|
||||
return obj.data.id;
|
||||
},
|
||||
|
||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||
getGoogleBrew : async (auth = defaultAuth, id, accessId, accessType)=>{
|
||||
const drive = googleDrive.drive({ version: 'v3', auth: auth });
|
||||
|
||||
const obj = await drive.files.get({
|
||||
fileId : id,
|
||||
|
||||
@@ -87,76 +87,68 @@ const api = {
|
||||
// Create middleware with the accessType passed in as part of the scope
|
||||
return async (req, res, next)=>{
|
||||
// Get relevant IDs for the brew
|
||||
const { id, googleId } = api.getId(req);
|
||||
let { id, googleId } = api.getId(req);
|
||||
|
||||
const accessMap = {
|
||||
edit : { editId: id },
|
||||
share : { shareId: id },
|
||||
admin : {
|
||||
$or : [
|
||||
{ editId: id },
|
||||
{ shareId: id },
|
||||
] }
|
||||
admin : { $or : [{ editId: id }, { shareId: id }] }
|
||||
};
|
||||
|
||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||
let stub = await HomebrewModel.get(accessMap[accessType])
|
||||
.catch((err)=>{
|
||||
if(googleId) {
|
||||
if(googleId)
|
||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||
} else {
|
||||
else
|
||||
console.warn(err);
|
||||
}
|
||||
});
|
||||
stub = stub?.toObject();
|
||||
googleId ??= stub?.googleId;
|
||||
|
||||
if(stub?.lock?.locked && accessType != 'edit') {
|
||||
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
||||
const isOwner = (accessType == 'edit' && (!stub || stub?.authors?.length === 0)) || stub?.authors?.[0] === req.account?.username;
|
||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||
|
||||
if(accessType === 'edit' && !(isOwner || isAuthor || isInvited)) {
|
||||
const accessError = { name: 'Access Error', status: 401, authors: stub?.authors, brewTitle: stub?.title, shareId: stub?.shareId };
|
||||
if(req.account)
|
||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03' };
|
||||
else
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04' };
|
||||
}
|
||||
|
||||
// If there is a google id, try to find the google brew
|
||||
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||
let googleError;
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||
.catch((err)=>{
|
||||
googleError = err;
|
||||
if(stub?.lock?.locked && accessType != 'edit') {
|
||||
throw { HBErrorCode: '51', code: stub?.lock.code, message: stub?.lock.shareMessage, brewId: stub?.shareId, brewTitle: stub?.title };
|
||||
}
|
||||
|
||||
// If there's a google id, get it if requesting the full brew or if no stub found yet
|
||||
if(googleId && (!stubOnly || !stub)) {
|
||||
const oAuth2Client = isOwner ? GoogleActions.authCheck(req.account, res) : undefined;
|
||||
|
||||
const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType)
|
||||
.catch((googleError)=>{
|
||||
const reason = googleError.errors?.[0].reason;
|
||||
if(reason == 'notFound')
|
||||
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||
else
|
||||
throw { ...googleError, HBErrorCode: '01' };
|
||||
});
|
||||
// Throw any error caught while attempting to retrieve Google brew.
|
||||
if(googleError) {
|
||||
const reason = googleError.errors?.[0].reason;
|
||||
if(reason == 'notFound') {
|
||||
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||
} else {
|
||||
throw { ...googleError, HBErrorCode: '01' };
|
||||
}
|
||||
}
|
||||
|
||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||
}
|
||||
const authorsExist = stub?.authors?.length > 0;
|
||||
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||
const accessError = { name: 'Access Error', status: 401 };
|
||||
if(req.account){
|
||||
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
||||
}
|
||||
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title, shareId: stub.shareId };
|
||||
}
|
||||
|
||||
// If after all of that we still don't have a brew, throw an exception
|
||||
if(!stub && !stubOnly) {
|
||||
if(!stub)
|
||||
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
||||
}
|
||||
|
||||
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||
if(stub) {
|
||||
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
}
|
||||
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
|
||||
req.brew = stub ?? {};
|
||||
req.brew = stub;
|
||||
next();
|
||||
};
|
||||
},
|
||||
@@ -475,12 +467,11 @@ const api = {
|
||||
}
|
||||
};
|
||||
|
||||
router.use('/api', checkClientVersion);
|
||||
router.post('/api', asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||
router.post('/api', checkClientVersion, asyncHandler(api.newBrew));
|
||||
router.put('/api/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.put('/api/update/:id', checkClientVersion, asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||
router.delete('/api/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/remove/:id', checkClientVersion, asyncHandler(api.deleteBrew));
|
||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
||||
|
||||
export default api;
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
||||
|
||||
describe('Tests for api', ()=>{
|
||||
let api;
|
||||
let google;
|
||||
@@ -296,7 +298,7 @@ describe('Tests for api', ()=>{
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(api.getId).toHaveBeenCalledWith(req);
|
||||
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||
expect(google.getGoogleBrew).toHaveBeenCalledWith(undefined, '2', '1', 'share');
|
||||
});
|
||||
|
||||
it('access is denied to a locked brew', async()=>{
|
||||
@@ -968,4 +970,57 @@ brew`);
|
||||
expect(res.send).toHaveBeenCalledWith('');
|
||||
});
|
||||
});
|
||||
describe('Split Text, Style, and Metadata', ()=>{
|
||||
|
||||
it('basic splitting', async ()=>{
|
||||
const testBrew = {
|
||||
text : '```metadata\n' +
|
||||
'title: title\n' +
|
||||
'description: description\n' +
|
||||
'tags: [ \'tag a\' , \'tag b\' ]\n' +
|
||||
'systems: [ test system ]\n' +
|
||||
'renderer: legacy\n' +
|
||||
'theme: 5ePHB\n' +
|
||||
'lang: en\n' +
|
||||
'\n' +
|
||||
'```\n' +
|
||||
'\n' +
|
||||
'```css\n' +
|
||||
'style\n' +
|
||||
'style\n' +
|
||||
'style\n' +
|
||||
'```\n' +
|
||||
'\n' +
|
||||
'text\n'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(testBrew);
|
||||
|
||||
// Metadata
|
||||
expect(testBrew.title).toEqual('title');
|
||||
expect(testBrew.description).toEqual('description');
|
||||
expect(testBrew.tags).toEqual(['tag a', 'tag b']);
|
||||
expect(testBrew.systems).toEqual(['test system']);
|
||||
expect(testBrew.renderer).toEqual('legacy');
|
||||
expect(testBrew.theme).toEqual('5ePHB');
|
||||
expect(testBrew.lang).toEqual('en');
|
||||
// Style
|
||||
expect(testBrew.style).toEqual('style\nstyle\nstyle');
|
||||
// Text
|
||||
expect(testBrew.text).toEqual('text\n');
|
||||
});
|
||||
|
||||
it('convert tags string to array', async ()=>{
|
||||
const testBrew = {
|
||||
text : '```metadata\n' +
|
||||
'tags: tag a\n' +
|
||||
'```\n\n'
|
||||
};
|
||||
|
||||
splitTextStyleAndMetadata(testBrew);
|
||||
|
||||
// Metadata
|
||||
expect(testBrew.tags).toEqual(['tag a']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import packageJSON from '../../package.json' with { type: "json" };
|
||||
const version = packageJSON.version;
|
||||
import packageJSON from '../../package.json' with { type: 'json' };
|
||||
|
||||
export default (req, res, next)=>{
|
||||
const userVersion = req.get('Homebrewery-Version');
|
||||
const version = packageJSON.version;
|
||||
|
||||
if(userVersion != version) {
|
||||
if(userVersion !== version) {
|
||||
return res.status(412).send({
|
||||
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||
});
|
||||
@@ -12,3 +12,4 @@ export default (req, res, next)=>{
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export default (req, res, next)=>{
|
||||
const isImageRequest = req.get('Accept')?.split(',')
|
||||
?.filter((h)=>!h.includes('q='))
|
||||
?.every((h)=>/image\/.*/.test(h));
|
||||
if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
|
||||
if(isImageRequest && !(isLocalEnvironment && req.url?.startsWith('/staticImages'))) {
|
||||
return res.status(406).send({
|
||||
message : 'Request for image at this URL is not supported'
|
||||
});
|
||||
|
||||
41
server/middleware/content-negotiation.spec.js
Normal file
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import contentNegotiationMiddleware from './content-negotiation.js';
|
||||
|
||||
describe('content-negotiation-middleware', ()=>{
|
||||
let request;
|
||||
let response;
|
||||
let next;
|
||||
|
||||
beforeEach(()=>{
|
||||
request = {
|
||||
get : function(key) {
|
||||
return this[key];
|
||||
}
|
||||
};
|
||||
response = {
|
||||
status : jest.fn(()=>response),
|
||||
send : jest.fn(()=>{})
|
||||
};
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
it('should return 406 on image request', ()=>{
|
||||
contentNegotiationMiddleware({
|
||||
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
...request
|
||||
}, response);
|
||||
|
||||
expect(response.status).toHaveBeenLastCalledWith(406);
|
||||
expect(response.send).toHaveBeenCalledWith({
|
||||
message : 'Request for image at this URL is not supported'
|
||||
});
|
||||
});
|
||||
|
||||
it('should call next on non-image request', ()=>{
|
||||
contentNegotiationMiddleware({
|
||||
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||
...request
|
||||
}, response, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,9 @@ const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.snippets = brew.text.slice(11, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
|
||||
// Handle old brews that still have empty strings in the tags metadata
|
||||
if(typeof brew.tags === 'string') brew.tags = brew.tags ? [brew.tags] : [];
|
||||
};
|
||||
|
||||
const printCurrentBrew = ()=>{
|
||||
|
||||
@@ -11,49 +11,54 @@
|
||||
@import (less) './themes/fonts/iconFonts/fontAwesome.less';
|
||||
|
||||
@keyframes sourceMoveAnimation {
|
||||
50% {background-color: red; color: white;}
|
||||
100% {background-color: unset; color: unset;}
|
||||
50% { color : white;background-color : red;}
|
||||
100% { color : unset;background-color : unset;}
|
||||
}
|
||||
|
||||
.codeEditor{
|
||||
@media screen and (pointer : coarse) {
|
||||
font-size : 16px;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
font-family: inherit;
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
color: grey;
|
||||
}
|
||||
.codeEditor {
|
||||
@media screen and (pointer : coarse) {
|
||||
font-size : 16px;
|
||||
}
|
||||
.CodeMirror-foldmarker {
|
||||
font-family : inherit;
|
||||
font-weight : 600;
|
||||
color : grey;
|
||||
text-shadow : none;
|
||||
}
|
||||
|
||||
.sourceMoveFlash .CodeMirror-line{
|
||||
animation-name: sourceMoveAnimation;
|
||||
animation-duration: 0.4s;
|
||||
}
|
||||
.CodeMirror-foldgutter {
|
||||
cursor : pointer;
|
||||
border-left : 1px solid #EEEEEE;
|
||||
transition : background 0.1s;
|
||||
&:hover { background : #DDDDDD; }
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 20px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width: 20px;
|
||||
background: linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||
}
|
||||
}
|
||||
.sourceMoveFlash .CodeMirror-line {
|
||||
animation-name : sourceMoveAnimation;
|
||||
animation-duration : 0.4s;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
&::-webkit-scrollbar { width : 20px; }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
width : 20px;
|
||||
background : linear-gradient(90deg, #858585 15px, #808080 15px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
|
||||
//.cm-trailingspace {
|
||||
// .cm-space {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
//.cm-trailingspace {
|
||||
// .cm-space {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
.emojiPreview {
|
||||
font-size: 1.5em;
|
||||
line-height: 1.2em;
|
||||
font-size : 1.5em;
|
||||
line-height : 1.2em;
|
||||
}
|
||||
@@ -43,5 +43,6 @@ html,body, #reactRoot{
|
||||
}
|
||||
&:disabled{
|
||||
background-color : @silver !important;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user