0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-25 20:33:13 +00:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Trevor Buckner
e4efc9f653 Make autosave common/move save button into common format 2025-08-30 16:28:54 -04:00
Trevor Buckner
cea342d7f6 Moving mount/unmount over. Missing states and imports. 2025-08-20 16:31:12 -04:00
Trevor Buckner
d443ecabae Move most common "handler" functions (textChange/splitMoves/etc.) 2025-08-20 16:30:44 -04:00
Trevor Buckner
91b6b9d91b Move most state over to common BaseEditPage 2025-08-20 16:29:29 -04:00
Trevor Buckner
7ca2123506 Move Splitpane / Editor / BrewRenderer into common BasePage
(not on /edit yet. That one is a beast.)
2025-08-20 16:28:05 -04:00
Trevor Buckner
4af33f6e75 Change fetchThemeBundle to not require this passed in.
`this` doesn't work with functional components
2025-08-20 16:25:54 -04:00
Trevor Buckner
90ceb52ffc Remove unused import 2025-08-11 21:30:51 -04:00
Trevor Buckner
6f9caf0590 Refactor to use BaseEditPage for shared layout
Some Nav buttons missing on the different pages should now appear in all three pages. Unique buttons are still only on those pages for now (/share nav button only appears on the /edit page, etc.)
2025-08-10 22:16:07 -04:00
37 changed files with 1245 additions and 1592 deletions

View File

@@ -49,7 +49,7 @@ Make an changes you need to `config/docker.json` then build the image. If it doe
"web_port" : 8000, "web_port" : 8000,
"enable_v3" : true, "enable_v3" : true,
"mongodb_uri": "mongodb://172.17.0.2/homebrewery", "mongodb_uri": "mongodb://172.17.0.2/homebrewery",
"enable_themes" : true "enable_themes" : true,
} }
``` ```
@@ -90,13 +90,6 @@ docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
``` ```
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
```shell
# Make sure you run this in the homebrewery directory
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```
## Updating the Image ## Updating the Image
When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image. When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image.
@@ -124,9 +117,3 @@ docker-compose build homebrewery
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
``` ```
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
```shell
# Make sure you run this in the homebrewery directory
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```

View File

@@ -75,9 +75,8 @@ it using the two commands:
1. `npm install` 1. `npm install`
1. `npm start` 1. `npm start`
When the Homebrewery server is started for the first time, it will modify the database to create the indexes required for better Homebrewery performance. This may take a few moments to complete for each index, dependent on how much content is in your local database - a brand new, empty database should be done in seconds. You should now be able to go to [http://localhost:8000](http://localhost:8000)
in your browser and use The Homebrewery offline.
On completion, you should be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use The Homebrewery offline.
If you had any issue at all, here are some links that may be useful: If you had any issue at all, here are some links that may be useful:
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners - [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
@@ -146,4 +145,3 @@ your contribution to the project, please join our [gitter chat][gitter-url].
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request [github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
[gitter-url]: https://gitter.im/naturalcrit/Lobby [gitter-url]: https://gitter.im/naturalcrit/Lobby

View File

@@ -7,17 +7,15 @@ import LockTools from './lockTools/lockTools.jsx';
const tabGroups = ['brew', 'notifications', 'authors', 'locks']; const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
const ADMIN_TAB = 'HB_adminPage_currentTab';
const Admin = ()=>{ const Admin = ()=>{
const [currentTab, setCurrentTab] = useState(''); const [currentTab, setCurrentTab] = useState('');
useEffect(()=>{ useEffect(()=>{
setCurrentTab(localStorage.getItem(ADMIN_TAB) || 'brew'); setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew');
}, []); }, []);
useEffect(()=>{ useEffect(()=>{
localStorage.setItem(ADMIN_TAB, currentTab); localStorage.setItem('hbAdminTab', currentTab);
}, [currentTab]); }, [currentTab]);
return ( return (

View File

@@ -2,8 +2,7 @@ require('./splitPane.less');
const React = require('react'); const React = require('react');
const { useState, useEffect } = React; const { useState, useEffect } = React;
const PANE_WIDTH_KEY = 'HB_editor_splitWidth'; const storageKey = 'naturalcrit-pane-split';
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
const SplitPane = (props)=>{ const SplitPane = (props)=>{
const { const {
@@ -19,9 +18,9 @@ const SplitPane = (props)=>{
const [liveScroll, setLiveScroll] = useState(false); const [liveScroll, setLiveScroll] = useState(false);
useEffect(()=>{ useEffect(()=>{
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY); const savedPos = window.localStorage.getItem(storageKey);
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2); setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true'); setLiveScroll(window.localStorage.getItem('liveScroll') === 'true');
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return ()=>window.removeEventListener('resize', handleResize); return ()=>window.removeEventListener('resize', handleResize);
@@ -30,13 +29,13 @@ const SplitPane = (props)=>{
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x))); const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position //when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13))); const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleUp =(e)=>{ const handleUp =(e)=>{
e.preventDefault(); e.preventDefault();
if(isDragging) { if(isDragging) {
onDragFinish(dividerPos); onDragFinish(dividerPos);
window.localStorage.setItem(PANE_WIDTH_KEY, dividerPos); window.localStorage.setItem(storageKey, dividerPos);
} }
setIsDragging(false); setIsDragging(false);
}; };
@@ -53,7 +52,7 @@ const SplitPane = (props)=>{
}; };
const liveScrollToggle = ()=>{ const liveScrollToggle = ()=>{
window.localStorage.setItem(LIVE_SCROLL_KEY, String(!liveScroll)); window.localStorage.setItem('liveScroll', String(!liveScroll));
setLiveScroll(!liveScroll); setLiveScroll(!liveScroll);
}; };

View File

@@ -24,8 +24,6 @@ const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m; const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent` const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head> <!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" /> <link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
@@ -41,8 +39,8 @@ const BrewPage = (props)=>{
index : 0, index : 0,
...props ...props
}; };
const pageRef = useRef(null); const pageRef = useRef(null);
const cleanText = safeHTML(props.contents); const cleanText = safeHTML(`${props.contents}\n<div class="columnSplit"></div>\n`);
useEffect(()=>{ useEffect(()=>{
if(!pageRef.current) return; if(!pageRef.current) return;
@@ -124,7 +122,7 @@ const BrewRenderer = (props)=>{
//useEffect to store or gather toolbar state from storage //useEffect to store or gather toolbar state from storage
useEffect(()=>{ useEffect(()=>{
const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY)); const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
toolbarState && setDisplayOptions(toolbarState); toolbarState && setDisplayOptions(toolbarState);
}, []); }, []);
@@ -286,7 +284,7 @@ const BrewRenderer = (props)=>{
const handleDisplayOptionsChange = (newDisplayOptions)=>{ const handleDisplayOptionsChange = (newDisplayOptions)=>{
setDisplayOptions(newDisplayOptions); setDisplayOptions(newDisplayOptions);
localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions)); localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
}; };
const pagesStyle = { const pagesStyle = {

View File

@@ -9,8 +9,6 @@ import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anch
const MAX_ZOOM = 300; const MAX_ZOOM = 300;
const MIN_ZOOM = 10; const MIN_ZOOM = 10;
const TOOLBAR_VISIBILITY = 'HB_renderer_toolbarVisibility';
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
const [pageNum, setPageNum] = useState(1); const [pageNum, setPageNum] = useState(1);
@@ -23,8 +21,8 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
}, [visiblePages]); }, [visiblePages]);
useEffect(()=>{ useEffect(()=>{
const Visibility = localStorage.getItem(TOOLBAR_VISIBILITY); const Visibility = localStorage.getItem('hb_toolbarVisibility');
if(Visibility) setToolsVisible(Visibility === 'true'); if (Visibility) setToolsVisible(Visibility === 'true');
}, []); }, []);
@@ -102,7 +100,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<div className='toggleButton'> <div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{ <button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
setToolsVisible(!toolsVisible); setToolsVisible(!toolsVisible);
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible); localStorage.setItem('hb_toolbarVisibility', !toolsVisible);
}}><i className='fas fa-glasses' /></button> }}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button> <button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div> </div>

View File

@@ -10,7 +10,7 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx'); const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HB_editor_theme'; const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -40,8 +40,11 @@ const Editor = createClass({
style : '' style : ''
}, },
onBrewChange : ()=>{}, onTextChange : ()=>{},
reportError : ()=>{}, onStyleChange : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{},
onCursorPageChange : ()=>{}, onCursorPageChange : ()=>{},
onViewPageChange : ()=>{}, onViewPageChange : ()=>{},
@@ -435,7 +438,7 @@ const Editor = createClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onBrewChange('text')} onChange={this.props.onTextChange}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} /> style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
@@ -448,7 +451,7 @@ const Editor = createClass({
language='css' language='css'
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange('style')} onChange={this.props.onStyleChange}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}
@@ -464,7 +467,7 @@ const Editor = createClass({
<MetadataEditor <MetadataEditor
metadata={this.props.brew} metadata={this.props.brew}
themeBundle={this.props.themeBundle} themeBundle={this.props.themeBundle}
onChange={this.props.onBrewChange('metadata')} onChange={this.props.onMetaChange}
reportError={this.props.reportError} reportError={this.props.reportError}
userThemes={this.props.userThemes}/> userThemes={this.props.userThemes}/>
</>; </>;
@@ -478,7 +481,7 @@ const Editor = createClass({
language='gfm' language='gfm'
view={this.state.view} view={this.state.view}
value={this.props.brew.snippets} value={this.props.brew.snippets}
onChange={this.props.onBrewChange('snippets')} onChange={this.props.onSnipChange}
enableFolding={true} enableFolding={true}
editorTheme={this.state.editorTheme} editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} rerenderParent={this.rerenderParent}

View File

@@ -4,8 +4,6 @@ import './homebrew.less';
import React from 'react'; import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
import HomePage from './pages/homePage/homePage.jsx'; import HomePage from './pages/homePage/homePage.jsx';
import EditPage from './pages/editPage/editPage.jsx'; import EditPage from './pages/editPage/editPage.jsx';
import UserPage from './pages/userPage/userPage.jsx'; import UserPage from './pages/userPage/userPage.jsx';
@@ -50,8 +48,6 @@ const Homebrew = (props)=>{
global.enable_themes = enable_themes; global.enable_themes = enable_themes;
global.config = config; global.config = config;
updateLocalStorage();
return ( return (
<Router location={url}> <Router location={url}>
<div className='homebrew'> <div className='homebrew'>

View File

@@ -1,138 +1,157 @@
require('./error-navitem.less'); require('./error-navitem.less');
const React = require('react'); const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const createClass = require('create-react-class');
const ErrorNavItem = ({error = '', clearError})=>{ const ErrorNavItem = createClass({
const response = error.response; getDefaultProps : function() {
const errorCode = error.code return {
const status = response?.status; error : '',
const HBErrorCode = response?.body?.HBErrorCode; parent : null
const message = response?.body?.message; };
},
render : function() {
const clearError = ()=>{
const state = {
error : null
};
if(this.props.parent.state.isSaving) {
state.isSaving = false;
}
this.props.parent.setState(state);
};
let errMsg = ''; const error = this.props.error;
try { const response = error.response;
errMsg += `${error.toString()}\n\n`; const status = response?.status;
errMsg += `\`\`\`\n${error.stack}\n`; const errorCode = error.code
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; const HBErrorCode = response?.body?.HBErrorCode;
console.log(errMsg); const message = response?.body?.message;
} catch (e){} let errMsg = '';
try {
errMsg += `${error.toString()}\n\n`;
errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
if(status === 409) { if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer' onClick={clearError}> <div className='errorContainer' onClick={clearError}>
{message ?? 'Conflict: please refresh to get latest changes'} {message ?? 'Conflict: please refresh to get latest changes'}
</div>
</Nav.item>;
}
if(status === 412) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
</div>
</Nav.item>;
}
if(HBErrorCode === '04') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
You are no longer signed in as an author of
this brew! Were you signed out from a different
window? Visit our log in page, then try again!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div> </div>
</div> </Nav.item>;
</Nav.item>; }
}
if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') { if(status === 412) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer' onClick={clearError}> <div className='errorContainer' onClick={clearError}>
Can't save because your Google Drive seems to be full! {message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
</div>
</Nav.item>;
}
if(response?.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Looks like your Google credentials have
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div> </div>
</div> </Nav.item>;
</Nav.item>; }
}
if(HBErrorCode === '04') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
You are no longer signed in as an author of
this brew! Were you signed out from a different
window? Visit our log in page, then try again!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Can't save because your Google Drive seems to be full!
</div>
</Nav.item>;
}
if(response?.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Looks like your Google credentials have
expired! Visit our log in page to sign out
and sign back in with Google,
then try saving again!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
if(HBErrorCode === '09') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Looks like there was a problem retreiving
the theme, or a theme that it inherits,
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
{response.body.brewId}</a> still exists!
</div>
</Nav.item>;
}
if(HBErrorCode === '10') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Looks like the brew you have selected
as a theme is not tagged for use as a
theme. Verify that
brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
{response.body.brewId}</a> has the <span className='lowercase'>meta:theme</span> tag!
</div>
</Nav.item>;
}
if(errorCode === 'ECONNABORTED') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
The request to the server was interrupted or timed out.
This can happen due to a network issue, or if
trying to save a particularly large brew.
Please check your internet connection and try again.
</div>
</Nav.item>;
}
if(HBErrorCode === '09') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer' onClick={clearError}> <div className='errorContainer'>
Looks like there was a problem retreiving Looks like there was a problem saving. <br />
the theme, or a theme that it inherits, Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}> here
{response.body.brewId}</a> still exists! </a>.
</div> </div>
</Nav.item>; </Nav.item>;
} }
});
if(HBErrorCode === '10') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Looks like the brew you have selected
as a theme is not tagged for use as a
theme. Verify that
brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
{response.body.brewId}</a> has the <span className='lowercase'>meta:theme</span> tag!
</div>
</Nav.item>;
}
if(errorCode === 'ECONNABORTED') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
The request to the server was interrupted or timed out.
This can happen due to a network issue, or if
trying to save a particularly large brew.
Please check your internet connection and try again.
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
here
</a>.
</div>
</Nav.item>;
};
module.exports = ErrorNavItem; module.exports = ErrorNavItem;

View File

@@ -5,45 +5,33 @@ const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); //
const BREWKEY = 'homebrewery-new'; const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta'; const METAKEY = 'homebrewery-new-meta';
const NewBrew = ()=>{ const NewBrew = ()=>{
const handleFileChange = (e)=>{ const handleFileChange = (e)=>{
const file = e.target.files[0]; const file = e.target.files[0];
if(!file) return; if(file) {
const reader = new FileReader();
const currentNew = localStorage.getItem(BREWKEY); reader.onload = (e)=>{
if(currentNew && !confirm( const fileContent = e.target.result;
`You have some text in the new brew space, if you load a file that text will be lost, are you sure you want to load the file?` const newBrew = {
)) return; text : fileContent,
style : ''
const reader = new FileReader(); };
reader.onload = (e)=>{ if(fileContent.startsWith('```metadata')) {
const fileContent = e.target.result; splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
const newBrew = { text: fileContent, style: '' }; localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style);
if(fileContent.startsWith('```metadata')) { localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
splitTextStyleAndMetadata(newBrew); window.location.href = '/new';
localStorage.setItem(BREWKEY, newBrew.text); } else {
localStorage.setItem(STYLEKEY, newBrew.style); alert('This file is invalid, please, enter a valid file');
localStorage.setItem(METAKEY, JSON.stringify( }
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']) };
)); reader.readAsText(file);
window.location.href = '/new'; }
return;
}
const type = file.name.split('.').pop().toLowerCase();
alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file);
};
reader.readAsText(file);
}; };
return ( return (
<Nav.dropdown> <Nav.dropdown>
<Nav.item <Nav.item

View File

@@ -5,8 +5,8 @@ const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const EDIT_KEY = 'HB_nav_recentlyEdited'; const EDIT_KEY = 'homebrewery-recently-edited';
const VIEW_KEY = 'HB_nav_recentlyViewed'; const VIEW_KEY = 'homebrewery-recently-viewed';
const RecentItems = createClass({ const RecentItems = createClass({

View File

@@ -1,35 +0,0 @@
import React from 'react';
import dedent from 'dedent-tabs';
import Nav from 'naturalcrit/nav/nav.jsx';
const getShareId = (brew)=>(
brew.googleId && !brew.stubbed
? brew.googleId + brew.shareId
: brew.shareId
);
const getRedditLink = (brew)=>{
const text = dedent`
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
export default ({brew}) => (
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item>
<Nav.item color='blue' href={`/share/${getShareId(brew)}`}>
view
</Nav.item>
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
copy url
</Nav.item>
<Nav.item color='blue' href={getRedditLink(brew)} newTab rel='noopener noreferrer'>
post to reddit
</Nav.item>
</Nav.dropdown>
);

View File

@@ -13,7 +13,7 @@ const AccountPage = (props)=>{
// initialize save location from local storage based on user id // initialize save location from local storage based on user id
React.useEffect(()=>{ React.useEffect(()=>{
if(!saveLocation && accountDetails.username) { if(!saveLocation && accountDetails.username) {
SAVEKEY = `HB_editor_defaultSave_${accountDetails.username}`; SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account. // if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
let saveLocation = window.localStorage.getItem(SAVEKEY); let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY'); saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');

View File

@@ -0,0 +1,309 @@
require('./editPage.less');
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../../navbar/navbar.jsx');
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
const PrintNavItem = require('../../../navbar/print.navitem.jsx');
const ErrorNavItem = require('../../../navbar/error-navitem.jsx');
const AccountNavItem = require('../../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../../navbar/vault.navitem.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const Editor = require('../../../editor/editor.jsx');
const BrewRenderer = require('../../../brewRenderer/brewRenderer.jsx');
const { fetchThemeBundle } = require('../../../../../shared/helpers.js');
import { useEffect, useState, useRef } from 'react';
import Markdown from 'naturalcrit/markdown.js';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
const SAVE_TIMEOUT = 10000;
const BaseEditPage = (props)=>{
const [brew, setBrew] = useState(() => props.brew);
const [savedBrew, setSavedBrew] = useState(brew);
const [isSaving, setIsSaving] = useState(false);
const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [saveGoogle, setSaveGoogle] = useState(() => (global.account?.googleId ? true : false));
const [welcomeText, setWelcomeText] = useState(() => props.brew?.text ?? '');
const [error, setError] = useState(undefined);
const [htmlErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(false);
const editorRef = useRef(null);
let lastSavedBrew = useRef(JSON.parse(JSON.stringify(this.propcopys.brew))); //Deep copy
const saveTimeout = useRef(null);
const unsavedChangesTimer = useRef(null);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
const handleTextChange = (text)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(htmlErrors.length)
htmlErrors = Markdown.validate(text);
setHTMLErrors(htmlErrors);
setBrew((prevBrew) => ({ ...prevBrew, text }));
// TODO: ONLY ON /NEW PAGE
localStorage.setItem(BREWKEY, text);
};
const handleStyleChange = (style)=>{
setBrew((prevBrew) => ({ ...prevBrew, style }));
if(props.useLocalStorage)
localStorage.setItem(STYLEKEY, style);
};
const handleSnipChange = (snippet)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(htmlErrors.length)
htmlErrors = Markdown.validate(text);
setHTMLErrors(htmlErrors);
setBrew((prevBrew) => ({ ...prevBrew, snippets: snippet }));
};
const handleMetaChange = (metadata, field=undefined)=>{
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setBrew((prevBrew) => ({ ...prevBrew, ...metadata }));
if(props.useLocalStorage)
localStorage.setItem(METAKEY, JSON.stringify({
'renderer' : metadata.renderer,
'theme' : metadata.theme,
'lang' : metadata.lang
}));
};
const updateBrew = (newData) => {
setBrew(prevBrew => ({ //TODO: May be able to just directly use setBrew instead of a wrapper, if its safe to assume we want all the data from newData
...prevBrew, //OR: Somehow combine handleTextChange, handleStyleChange, handleMetaChange, and handleSnipChange into one function that calls this
style: newData.style,
text: newData.text,
snippets: newData.snippets
}));
};
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const save = async (immediate=false)=>{
if(isSaving) return;
if(!unsavedChanges && !immediate) return;
clearTimeout(saveTimeout.current);
const timeout = immediate ? 0 : 10000;
saveTimeout.current = setTimeout(async () => {
setIsSaving(true);
await props.performSave(brew, saveGoogle)
.catch((err)=>{
setError(err);
});
setIsSaving(false);
setLastSavedTime(new Date());
setTimeout(setWarnUnsavedChanges(true), 900000); // 15 minutes between unsaved work warnings
}, timeout);
};
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) save();
if(e.keyCode == P_KEY) BrewRenderer.printCurrentBrew();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
};
const hasChanges =()=>{
return !_.isEqual(brew, savedBrew);
};
useEffect(() => {
props.loadBrew?.(brew, setBrew, setSaveGoogle); //Initial load from localStorage/etc.
//Load settings
setAutoSaveEnabled(JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true);
setHTMLErrors(Markdown.validate(brew.text));
document.addEventListener('keydown', handleControlKeys);
window.onbeforeunload = ()=>{
if(isSaving || unsavedChanges)
return 'You have unsaved changes!';
};
return () => {
document.removeEventListener('keydown', handleControlKeys);
window.onbeforeunload = null;
}
}, []);
useEffect(() => {
fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme);
}, [brew.renderer, brew.theme]);
useEffect(() => {
const hasChange = hasChanges();
if(unsavedChanges !== hasChange)
setUnsavedChanges(hasChange);
if(autoSaveEnabled) save();
}, [brew]);
const resetUnsavedChangesWarning = ()=>{
setTimeout(setWarnUnsavedChanges(false), 4000); // Display warning for 4 seconds
setTimeout(setWarnUnsavedChanges(true) , 90000); // 15 minutes between warnings
};
const toggleAutoSave = ()=>{
if(unsavedChangesTimer.current) clearTimeout(unsavedChangesTimer.current);
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled));
setAutoSaveEnabled(!autoSaveEnabled);
setWarnUnsavedChanges(false);
};
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
if(unsavedChanges && warnUnsavedChanges){
resetUnsavedChangesWarning();
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>{text}</div>
</Nav.item>;
}
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={()=>save(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved.</Nav.item>;
};
const renderAutoSaveButton = ()=>{
return <Nav.item onClick={toggleAutoSave}>
Autosave <i className={autosaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>;
};
return (
<div className={`sitePage ${props.className || ''}`}>
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{props.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{error
? <ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>
: <Nav.dropdown className='save-menu'>
{renderSaveButton()}
{renderAutoSaveButton()}
</Nav.dropdown>
}
{props.renderUniqueNav?.()}
</Nav.section>
<Nav.section>
<PrintNavItem />
<NewBrewItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem brew={props.brew} storageKey={props.recentStorageKey} />
<AccountNavItem />
</Nav.section>
</Navbar>
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={brew}
onTextChange={handleTextChange}
onStyleChange={handleStyleChange}
onMetaChange={handleMetaChange}
onSnipChange={handleSnipChange}
reportError={this.errorReported}
renderer={brew.renderer}
showEditButtons={false} //FALSE FOR HOME PAGE
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={brew.text}
style={brew.style}
renderer={brew.renderer}
theme={brew.theme}
errors={htmlErrors}
lang={brew.lang}
onPageChange={handleBrewRendererPageChange}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
themeBundle={themeBundle}
allowPrint={true} // FALSE FOR HOME PAGE
/>
</SplitPane>
</div>
{props.children?.(welcomeText, brew, save)}
</div>
);
};
module.exports = BaseEditPage;

View File

@@ -7,9 +7,7 @@ const moment = require('moment');
const BrewItem = require('./brewItem/brewItem.jsx'); const BrewItem = require('./brewItem/brewItem.jsx');
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir'; const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
const DEFAULT_SORT_TYPE = 'alpha'; const DEFAULT_SORT_TYPE = 'alpha';
const DEFAULT_SORT_DIR = 'asc'; const DEFAULT_SORT_DIR = 'asc';
@@ -52,12 +50,12 @@ const ListPage = createClass({
// LOAD FROM LOCAL STORAGE // LOAD FROM LOCAL STORAGE
if(typeof window !== 'undefined') { if(typeof window !== 'undefined') {
const newSortType = (this.state.sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE)); const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR)); const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
this.updateUrl(this.state.filterString, newSortType, newSortDir); this.updateUrl(this.state.filterString, newSortType, newSortDir);
const brewCollection = this.props.brewCollection.map((brewGroup)=>{ const brewCollection = this.props.brewCollection.map((brewGroup)=>{
brewGroup.visible = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true')=='true'; brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
return brewGroup; return brewGroup;
}); });
@@ -75,10 +73,10 @@ const ListPage = createClass({
saveToLocalStorage : function() { saveToLocalStorage : function() {
this.state.brewCollection.map((brewGroup)=>{ this.state.brewCollection.map((brewGroup)=>{
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${brewGroup.visible}`); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
}); });
localStorage.setItem(USERPAGE_SORT_TYPE, this.state.sortType); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
localStorage.setItem(USERPAGE_SORT_DIR, this.state.sortDir); localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
}, },
renderBrews : function(brews){ renderBrews : function(brews){

View File

@@ -29,7 +29,6 @@
&::before { &::before {
margin-right : 5px; margin-right : 5px;
font-family : 'Font Awesome 6 Free'; font-family : 'Font Awesome 6 Free';
font-weight : 900;
content : '\f00c'; content : '\f00c';
} }
} }

View File

@@ -1,411 +1,253 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import './editPage.less'; require('./editPage.less');
const React = require('react');
const _ = require('lodash');
const createClass = require('create-react-class');
import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
// Common imports import request from '../../utils/request-middleware.js';
import React, { useState, useEffect, useRef } from 'react'; const { Meta } = require('vitreum/headtags');
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; const Nav = require('naturalcrit/nav/nav.jsx');
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; const BaseEditPage = require('../basePages/editPage/editPage.jsx');
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'naturalcrit/nav/nav.jsx'; const LockNotification = require('./lockNotification/lockNotification.jsx');
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports import Markdown from 'naturalcrit/markdown.js';
import { Meta } from 'vitreum/headtags';
import _ from 'lodash'; const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
import { md5 } from 'hash-wasm'; const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import ShareNavItem from '../../navbar/share.navitem.jsx';
import LockNotification from './lockNotification/lockNotification.jsx';
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg';
const SAVE_TIMEOUT = 10000; const googleDriveIcon = require('../../googleDrive.svg');
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; const EditPage = createClass({
const BREWKEY = 'HB_newPage_content'; displayName : 'EditPage',
const STYLEKEY = 'HB_newPage_style'; getDefaultProps : function() {
const SNIPKEY = 'HB_newPage_snippets'; return {
const METAKEY = 'HB_newPage_meta'; brew : DEFAULT_BREW_LOAD
const useLocalStorage = false;
const EditPage = (props)=>{
props = {
brew : DEFAULT_BREW_LOAD,
...props
};
const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false);
const [lastSavedTime , setLastSavedTime ] = useState(new Date());
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const saveTimeout = useRef(null);
const warnUnsavedTimeout = useRef(null);
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
setAutoSaveEnabled(autoSavePref);
setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(Markdown.validate(currentBrew.text));
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
}; };
},
document.addEventListener('keydown', handleControlKeys); getInitialState : function() {
window.onbeforeunload = ()=>{ return {
if(unsavedChangesRef.current) alertTrashedGoogleBrew : this.props.brew.trashed,
return 'You have unsaved changes!'; alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
error : null,
displayLockMessage : this.props.brew.lock || false,
}; };
return ()=>{ },
document.removeEventListener('keydown', handleControlKeys);
window.onBeforeUnload = null;
};
}, []);
useEffect(()=>{ componentDidMount : function(){
trySaveRef.current = trySave;
unsavedChangesRef.current = unsavedChanges;
});
useEffect(()=>{ },
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange); handleGoogleClick : function(){
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current?.update();
};
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
...prevBrew,
style : newData.style,
text : newData.text,
snippets : newData.snippets
}));
const resetWarnUnsavedTimer = ()=>{
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
clearTimeout(warnUnsavedTimeout.current);
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
};
const handleGoogleClick = ()=>{
if(!global.account?.googleId) { if(!global.account?.googleId) {
setAlertLoginToTransfer(true); this.setState({
alertLoginToTransfer : true
});
return; return;
} }
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer,
error : null
}));
setConfirmGoogleTransfer((prev)=>!prev); },
setError(null);
};
const closeAlerts = (e)=>{ closeAlerts : function(event){
e.stopPropagation(); //Only handle click once so alert doesn't reopen event.stopPropagation(); //Only handle click once so alert doesn't reopen
setAlertTrashedGoogleBrew(false); this.setState({
setAlertLoginToTransfer(false); alertTrashedGoogleBrew : false,
setConfirmGoogleTransfer(false); alertLoginToTransfer : false,
}; confirmGoogleTransfer : false
});
},
const toggleGoogleStorage = ()=>{ toggleGoogleStorage : function(){
setSaveGoogle((prev)=>!prev); this.setState((prevState)=>({
setError(null); saveGoogle : !prevState.saveGoogle,
trySave(true); error : null
}; }), ()=>this.trySave(true));
},
const trySave = (immediate = false, hasChanges = true)=>{ save : async function(){
clearTimeout(saveTimeout.current); if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
if(isSaving) return;
if(!hasChanges && !immediate) return;
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
saveTimeout.current = setTimeout(async ()=>{ const brewState = this.state.brew; // freeze the current state
setIsSaving(true); const preSaveSnapshot = { ...brewState };
setError(null);
await save(currentBrew, saveGoogle)
.catch((err)=>{
setError(err);
});
setIsSaving(false);
setLastSavedTime(new Date());
if(!autoSaveEnabled) resetWarnUnsavedTimer();
}, newTimeout);
};
const save = async (brew, saveToGoogle)=>{ this.setState((prevState)=>({
setHTMLErrors(Markdown.validate(brew.text)); isSaving : true,
error : null,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
await updateHistory(brew).catch(console.error); await updateHistory(this.state.brew).catch(console.error);
await versionHistoryGarbageCollection().catch(console.error); await versionHistoryGarbageCollection().catch(console.error);
//Prepare content to send to server //Prepare content to send to server
const brewToSave = { const brew = { ...brewState };
...brew, brew.text = brew.text.normalize('NFC');
text : brew.text.normalize('NFC'), this.savedBrew.text = this.savedBrew.text.normalize('NFC');
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text)));
hash : await md5(lastSavedBrew.current.text), brew.hash = await md5(this.savedBrew.text);
textBin : undefined, //brew.text = undefined; - Temporary parallel path
version : lastSavedBrew.current.version brew.textBin = undefined;
};
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); const compressedBrew = gzipSync(strToU8(JSON.stringify(brew)));
const transfer = saveToGoogle === _.isNil(brew.googleId);
const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : '';
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
const res = await request const res = await request
.put(`/api/update/${brewToSave.editId}${params}`) .put(`/api/update/${brew.editId}${params}`)
.set('Content-Encoding', 'gzip') .set('Content-Encoding', 'gzip')
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send(compressedBrew) .send(compressedBrew)
.catch((err)=>{ .catch((err)=>{
console.error('Error Updating Local Brew'); console.log('Error Updating Local Brew');
setError(err); this.setState({ error: err });
}); });
if(!res) return; if(!res) return;
const updatedFields = { this.savedBrew = {
googleId : res.body.googleId ?? null, ...preSaveSnapshot,
editId : res.body.editId, googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId,
shareId : res.body.shareId, shareId : res.body.shareId,
version : res.body.version version : res.body.version
}; };
lastSavedBrew.current = { this.setState((prevState) => ({
...brew, brew: {
...updatedFields ...prevState.brew,
}; googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId,
shareId : res.body.shareId,
version : res.body.version
},
isSaving : false,
}), ()=>{
this.setState({ unsavedChanges : this.hasChanges() });
});
setCurrentBrew((prevBrew)=>({ history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
...prevBrew, },
...updatedFields
}));
history.replaceState(null, null, `/edit/${res.body.editId}`); renderGoogleDriveIcon : function(){
}; return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
const renderGoogleDriveIcon = ()=>( {this.state.confirmGoogleTransfer &&
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}> <div className='errorContainer' onClick={this.closeAlerts}>
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' /> { this.state.saveGoogle
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
{confirmGoogleTransfer && ( : `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
<div className='errorContainer' onClick={closeAlerts}> }
{saveGoogle
? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?'
: 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
<br /> <br />
<div className='confirm' onClick={toggleGoogleStorage}> Yes </div> <div className='confirm' onClick={this.toggleGoogleStorage}>
<div className='deny'> No </div> Yes
</div>
<div className='deny'>
No
</div>
</div> </div>
)} }
{alertLoginToTransfer && ( {this.state.alertLoginToTransfer &&
<div className='errorContainer' onClick={closeAlerts}> <div className='errorContainer' onClick={this.closeAlerts}>
You must be signed in to a Google account to transfer between the homebrewery and Google Drive! You must be signed in to a Google account to transfer
<a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}> between the homebrewery and Google Drive!
<div className='confirm'> Sign In </div> <a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.Location.href}`}>
<div className='confirm'>
Sign In
</div>
</a> </a>
<div className='deny'> Not Now </div> <div className='deny'>
Not Now
</div>
</div> </div>
)} }
{alertTrashedGoogleBrew && ( {this.state.alertTrashedGoogleBrew &&
<div className='errorContainer' onClick={closeAlerts}> <div className='errorContainer' onClick={this.closeAlerts}>
This brew is currently in your Trash folder on Google Drive!<br /> This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
If you want to keep it, make sure to move it before it is deleted permanently!<br /> <div className='confirm'>
<div className='confirm'> OK </div> OK
</div>
</div> </div>
)} }
</Nav.item> </Nav.item>;
); },
const renderSaveButton = ()=>{ processShareId : function() {
// #1 - Currently saving, show SAVING return this.state.brew.googleId && !this.state.brew.stubbed ?
if(isSaving) this.state.brew.googleId + this.state.brew.shareId :
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>; this.state.brew.shareId;
},
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING getRedditLink : function(){
if(unsavedChanges && warnUnsavedChanges) { const shareLink = this.processShareId();
resetWarnUnsavedTimer(); const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const title = `${this.props.brew.title} ${systems}`;
const text = elapsedTime === 0 const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
? 'Autosave is OFF.'
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'> **[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
Reminder...
<div className='errorContainer'>{text}</div>
</Nav.item>;
}
// #3 - Unsaved changes exist, click to save, show SAVE NOW return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
if(unsavedChanges) },
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED renderNavbar : function(){
if(autoSaveEnabled) const shareLink = this.processShareId();
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED return <>
return <Nav.item className='save saved'>saved.</Nav.item>;
};
const toggleAutoSave = ()=>{
clearTimeout(warnUnsavedTimeout.current);
clearTimeout(saveTimeout.current);
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
setAutoSaveEnabled(!autoSaveEnabled);
setWarnUnsavedChanges(autoSaveEnabled);
};
const renderAutoSaveButton = ()=>(
<Nav.item onClick={toggleAutoSave}>
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>
);
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = ()=>{
return <Navbar>
<Nav.section> <Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item> {this.renderGoogleDriveIcon()}
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item>
<Nav.item color='blue' href={`/share/${shareLink}`}>
view
</Nav.item>
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
copy url
</Nav.item>
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
post to reddit
</Nav.item>
</Nav.dropdown>
</Nav.section> </Nav.section>
</>;
},
<Nav.section> render : function(){
{renderGoogleDriveIcon()} return <BaseEditPage
{error className="editPage"
? <ErrorNavItem error={error} clearError={clearError} /> errorState={this.state.error}
: <Nav.dropdown className='save-menu'> parent={this}
{renderSaveButton()} brew={this.state.brew}
{renderAutoSaveButton()} renderUniqueNav={this.renderNavbar}
</Nav.dropdown>} performSave={this.save}
<NewBrewItem /> recentStorageKey='edit'>
<PrintNavItem /> {(welcomeText, brew, save) => {
<HelpNavItem /> return <>
<VaultNavItem /> <Meta name='robots' content='noindex, nofollow' />
<ShareNavItem brew={currentBrew} /> {this.props.brew.lock && <LockNotification shareId={brew.shareId} message={brew.lock.editMessage} reviewRequested={brew.lock.reviewRequested} />}
<RecentNavItem brew={currentBrew} storageKey='edit' /> </>
<AccountNavItem/> }}
</Nav.section> </BaseEditPage>;
</Navbar>; }
}; });
return (
<div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
{renderNavbar()}
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
reportError={setError}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
theme={currentBrew.theme}
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>
);
};
module.exports = EditPage; module.exports = EditPage;

View File

@@ -1,179 +1,54 @@
/* eslint-disable max-lines */ require('./homePage.less');
import './homePage.less'; const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags');
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; const BaseEditPage = require('../basePages/editPage/editPage.jsx');
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'naturalcrit/nav/nav.jsx'; const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports const HomePage = createClass({
import { Meta } from 'vitreum/headtags'; displayName : 'HomePage',
getDefaultProps : function() {
const BREWKEY = 'homebrewery-new'; return {
const STYLEKEY = 'homebrewery-new-style'; brew : DEFAULT_BREW
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const useLocalStorage = false;
const HomePage =(props)=>{
props = {
brew : DEFAULT_BREW,
ver : '0.0.0',
...props
};
const [currentBrew , setCurrentBrew] = useState(props.brew);
const [welcomeText , setWelcomeText] = useState(props.brew.text);
const [error , setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({});
const [isSaving , setIsSaving] = useState(false);
const editorRef = useRef(null);
useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
}; };
},
document.addEventListener('keydown', handleControlKeys); save : function(brew){
return request
return () => { .post('/api')
document.removeEventListener('keydown', handleControlKeys); .send(brew)
}; .then((res) => {
}, []);
const save = ()=>{
request.post('/api')
.send(currentBrew)
.end((err, res)=>{
if(err) {
setError(err);
return;
}
const saved = res.body; const saved = res.body;
window.location = `/edit/${saved.editId}`; window.location = `/edit/${saved.editId}`;
}); });
}; },
const handleSplitMove = ()=>{ render : function(){
editorRef.current.update(); return <BaseEditPage
}; {...this.props}
className="homePage"
parent={this}
performSave={this.save}>
{(welcomeText, brew, save) => {
return <>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
<div className={cx('floatingSaveButton', { show: welcomeText != brew.text })} onClick={save}>
Save current <i className='fas fa-save' />
</div>
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' <a href='/new' className='floatingNewButton'>
if (subfield == 'renderer' || subfield == 'theme') Create your own <i className='fas fa-magic' />
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); </a>
</>
//If there are HTML errors, run the validator on every change to give quick feedback }}
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) </BaseEditPage>
setHTMLErrors(Markdown.validate(value)); }
});
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = ()=>{
return <Navbar ver={props.ver}>
<Nav.section>
{error ?
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
null
}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
};
return (
<div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
showEditButtons={false}
themeBundle={themeBundle}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
themeBundle={themeBundle}
/>
</SplitPane>
</div>
<div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}>
Save current <i className='fas fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' />
</a>
</div>
)
};
module.exports = HomePage; module.exports = HomePage;

View File

@@ -1,83 +1,31 @@
/* eslint-disable max-lines */ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import './newPage.less'; require('./newPage.less');
const React = require('react');
const createClass = require('create-react-class');
import request from '../../utils/request-middleware.js';
// Common imports const Nav = require('naturalcrit/nav/nav.jsx');
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; const BaseEditPage = require('../basePages/editPage/editPage.jsx');
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from 'naturalcrit/nav/nav.jsx'; const BREWKEY = 'homebrewery-new';
import Navbar from '../../navbar/navbar.jsx'; const STYLEKEY = 'homebrewery-new-style';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; const METAKEY = 'homebrewery-new-meta';
import AccountNavItem from '../../navbar/account.navitem.jsx'; const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports const NewPage = createClass({
import { Meta } from 'vitreum/headtags'; displayName : 'NewPage',
getDefaultProps : function() {
return {
const BREWKEY = 'HB_newPage_content'; brew : DEFAULT_BREW
const STYLEKEY = 'HB_newPage_style';
const METAKEY = 'HB_newPage_metadata';
const SNIPKEY = 'HB_newPage_snippets';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true;
const NewPage = (props) => {
props = {
brew: DEFAULT_BREW,
...props
};
const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false);
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const editorRef = useRef(null);
useEffect(() => {
loadBrew();
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
}; };
},
document.addEventListener('keydown', handleControlKeys); loadBrew : function(brew, setBrew, setSaveGoogle) {
return () => {
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
const loadBrew = ()=>{
const brew = { ...currentBrew };
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
//TODO: Move localstorage handling to BaseEditPage?
const brewStorage = localStorage.getItem(BREWKEY); const brewStorage = localStorage.getItem(BREWKEY);
const styleStorage = localStorage.getItem(STYLEKEY); const styleStorage = localStorage.getItem(STYLEKEY);
const metaStorage = JSON.parse(localStorage.getItem(METAKEY)); const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
@@ -89,148 +37,44 @@ const NewPage = (props) => {
brew.lang = metaStorage?.lang ?? brew.lang; brew.lang = metaStorage?.lang ?? brew.lang;
} }
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew); setBrew(brew);
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle); setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle);
localStorage.setItem(BREWKEY, brew.text); localStorage.setItem(BREWKEY, brew.text);
if(brew.style) if(brew.style)
localStorage.setItem(STYLEKEY, brew.style); localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang })); localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
if(window.location.pathname !== '/new') if(window.location.pathname != '/new') {
window.history.replaceState({}, window.location.title, '/new/'); window.history.replaceState({}, window.location.title, '/new/');
};
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
if (subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
} }
}; },
const save = async () => { save : async function(brew, saveGoogle){
setIsSaving(true); brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
return request
let updatedBrew = { ...currentBrew };
splitTextStyleAndMetadata(updatedBrew);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
const res = await request
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(updatedBrew) .send(brew)
.catch((err) => { .then((res) => {
setIsSaving(false); //TODO: Move localstorage handling to BaseEditPage?
setError(err); localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY);
const saved = res.body;
window.location = `/edit/${saved.editId}`;
}); });
},
setIsSaving(false) render : function(){
if (!res) return; return <BaseEditPage
{...this.props}
const savedBrew = res.body; className="newPage"
parent={this}
localStorage.removeItem(BREWKEY); performSave={this.save}
localStorage.removeItem(STYLEKEY); loadBrew={this.loadBrew}>
localStorage.removeItem(METAKEY); </BaseEditPage>;
window.location = `/edit/${savedBrew.editId}`; }
}; });
const renderSaveButton = ()=>{
if(isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
save
</Nav.item>;
}
};
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = () => (
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</Nav.section>
<Nav.section>
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>
);
return (
<div className='newPage sitePage'>
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
theme={currentBrew.theme}
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>
);
};
module.exports = NewPage; module.exports = NewPage;

View File

@@ -17,11 +17,15 @@ const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpe
const SharePage = (props)=>{ const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props; const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
const [themeBundle, setThemeBundle] = useState({}); const [state, setState] = useState({
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); themeBundle : {},
currentBrewRendererPageNum : 1,
});
const handleBrewRendererPageChange = useCallback((pageNumber)=>{ const handleBrewRendererPageChange = useCallback((pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber); setState((prevState)=>({
currentBrewRendererPageNum : pageNumber,
...prevState }));
}, []); }, []);
const handleControlKeys = (e)=>{ const handleControlKeys = (e)=>{
@@ -36,7 +40,11 @@ const SharePage = (props)=>{
useEffect(()=>{ useEffect(()=>{
document.addEventListener('keydown', handleControlKeys); document.addEventListener('keydown', handleControlKeys);
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme); fetchThemeBundle(
{ setState },
brew.renderer,
brew.theme
);
return ()=>{ return ()=>{
document.removeEventListener('keydown', handleControlKeys); document.removeEventListener('keydown', handleControlKeys);
@@ -106,9 +114,9 @@ const SharePage = (props)=>{
lang={brew.lang} lang={brew.lang}
renderer={brew.renderer} renderer={brew.renderer}
theme={brew.theme} theme={brew.theme}
themeBundle={themeBundle} themeBundle={state.themeBundle}
onPageChange={handleBrewRendererPageChange} onPageChange={handleBrewRendererPageChange}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</div> </div>

View File

@@ -39,14 +39,10 @@ const UserPage = (props)=>{
}] : []) }] : [])
]; ];
const clearError = ()=>{
setError(null);
};
const navItems = ( const navItems = (
<Navbar> <Navbar>
<Nav.section> <Nav.section>
{error && (<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>)} {error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
<NewBrew /> <NewBrew />
<HelpNavItem /> <HelpNavItem />
<VaultNavitem /> <VaultNavitem />

View File

@@ -1,74 +0,0 @@
import requestMiddleware from './request-middleware';
jest.mock('superagent');
import request from 'superagent';
describe('request-middleware', ()=>{
let version;
let setFn;
let testFn;
beforeEach(()=>{
jest.resetAllMocks();
version = global.version;
global.version = '999';
setFn = jest.fn();
testFn = jest.fn(()=>{ return { set: setFn }; });
});
afterEach(()=>{
global.version = version;
});
it('should add header to get', ()=>{
// Ensure tests functions have been reset
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.get = testFn;
requestMiddleware.get('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
it('should add header to put', ()=>{
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.put = testFn;
requestMiddleware.put('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
it('should add header to post', ()=>{
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.post = testFn;
requestMiddleware.post('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
it('should add header to delete', ()=>{
expect(testFn).not.toHaveBeenCalled();
expect(setFn).not.toHaveBeenCalled();
request.delete = testFn;
requestMiddleware.delete('path');
expect(testFn).toHaveBeenCalledWith('path');
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
});
});

View File

@@ -1,35 +0,0 @@
const getLocalStorageMap = function(){
const localStorageMap = {
'AUTOSAVE_ON' : 'HB_editor_autoSaveOn',
'HOMEBREWERY-EDITOR-THEME' : 'HB_editor_theme',
'liveScroll' : 'HB_editor_liveScroll',
'naturalcrit-pane-split' : 'HB_editor_splitWidth',
'HOMEBREWERY-LISTPAGE-SORTDIR' : 'HB_listPage_sortDir',
'HOMEBREWERY-LISTPAGE-SORTTYPE' : 'HB_listPage_sortType',
'HOMEBREWERY-LISTPAGE-VISIBILITY-published' : 'HB_listPage_visibility_group_published',
'HOMEBREWERY-LISTPAGE-VISIBILITY-unpublished' : 'HB_listPage_visibility_group_unpublished',
'hbAdminTab' : 'HB_adminPage_currentTab',
'homebrewery-new' : 'HB_newPage_content',
'homebrewery-new-meta' : 'HB_newPage_metadata',
'homebrewery-new-style' : 'HB_newPage_style',
'homebrewery-recently-edited' : 'HB_nav_recentlyEdited',
'homebrewery-recently-viewed' : 'HB_nav_recentlyViewed',
'hb_toolbarState' : 'HB_renderer_toolbarState',
'hb_toolbarVisibility' : 'HB_renderer_toolbarVisibility'
};
if(global?.account?.username){
const username = global.account.username;
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
}
return localStorageMap;
};
export default getLocalStorageMap;

View File

@@ -1,30 +0,0 @@
import getLocalStorageMap from './localStorageKeyMap.js';
describe('getLocalStorageMap', ()=>{
it('no username', ()=>{
const account = global.account;
delete global.account;
const map = getLocalStorageMap();
global.account = account;
expect(map).toBeInstanceOf(Object);
expect(Object.entries(map)).toHaveLength(16);
});
it('no username', ()=>{
const account = global.account;
global.account = { username: 'test' };
const map = getLocalStorageMap();
global.account = account;
expect(map).toBeInstanceOf(Object);
expect(Object.entries(map)).toHaveLength(17);
expect(map).toHaveProperty('HOMEBREWERY-DEFAULT-SAVE-LOCATION-test', 'HB_editor_defaultSave_test');
});
});

View File

@@ -1,25 +0,0 @@
import getLocalStorageMap from './localStorageKeyMap.js';
const updateLocalStorage = function(){
// Return if no window and thus no local storage
if(typeof window === 'undefined') return;
// Return if the local storage key map has no content
const localStorageKeyMap = getLocalStorageMap();
if(Object.keys(localStorageKeyMap).length == 0) return;
const storage = window.localStorage;
Object.keys(localStorageKeyMap).forEach((key)=>{
if(storage[key]){
if(!storage[localStorageKeyMap[key]]){
const data = storage.getItem(key);
storage.setItem(localStorageKeyMap[key], data);
};
storage.removeItem(key);
}
});
};
export { updateLocalStorage };

777
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -136,19 +136,19 @@
"written-number": "^0.11.1" "written-number": "^0.11.1"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^4.0.0", "@stylistic/stylelint-plugin": "^3.1.3",
"babel-plugin-transform-import-meta": "^2.3.3", "babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.35.0", "eslint": "^9.31.0",
"eslint-plugin-jest": "^29.0.1", "eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0", "globals": "^16.3.0",
"jest": "^30.1.3", "jest": "^30.0.5",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.24.0", "stylelint": "^16.22.0",
"stylelint-config-recess-order": "^7.3.0", "stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^17.0.0", "stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.4" "supertest": "^7.1.4"
} }
} }

View File

@@ -487,8 +487,8 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
const query = { authors: req.account.username, googleId: { $exists: false } }; const query = { authors: req.account.username, googleId: { $exists: false } };
const mongoCount = await HomebrewModel.countDocuments(query) const mongoCount = await HomebrewModel.countDocuments(query)
.catch((err)=>{ .catch((err)=>{
mongoCount = 0;
console.log(err); console.log(err);
return 0;
}); });
data.accountDetails = { data.accountDetails = {

View File

@@ -27,10 +27,7 @@ const disconnect = async ()=>{
}; };
const connect = async (config)=>{ const connect = async (config)=>{
return await Mongoose.connect(getMongoDBURL(config), { return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
retryWrites : false,
autoIndex : (config.get('local_environments').includes(config.get('node_env')))
})
.catch((error)=>handleConnectionError(error)); .catch((error)=>handleConnectionError(error));
}; };

View File

@@ -7,29 +7,29 @@ import zlib from 'zlib';
const HomebrewSchema = mongoose.Schema({ const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } }, editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
googleId : { type: String, index: true }, googleId : { type: String },
title : { type: String, default: '', index: true }, title : { type: String, default: '' },
text : { type: String, default: '' }, text : { type: String, default: '' },
textBin : { type: Buffer }, textBin : { type: Buffer },
pageCount : { type: Number, default: 1, index: true }, pageCount : { type: Number, default: 1 },
description : { type: String, default: '' }, description : { type: String, default: '' },
tags : { type: [String], index: true }, tags : [String],
systems : [String], systems : [String],
lang : { type: String, default: 'en', index: true }, lang : { type: String, default: 'en' },
renderer : { type: String, default: '', index: true }, renderer : { type: String, default: '' },
authors : { type: [String], index: true }, authors : [String],
invitedAuthors : [String], invitedAuthors : [String],
published : { type: Boolean, default: false, index: true }, published : { type: Boolean, default: false },
thumbnail : { type: String, default: '', index: true }, thumbnail : { type: String, default: '' },
createdAt : { type: Date, default: Date.now, index: true }, createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now, index: true }, updatedAt : { type: Date, default: Date.now },
lastViewed : { type: Date, default: Date.now, index: true }, lastViewed : { type: Date, default: Date.now },
views : { type: Number, default: 0 }, views : { type: Number, default: 0 },
version : { type: Number, default: 1, index: true }, version : { type: Number, default: 1 },
lock : { type: Object, index: true } lock : { type: Object }
}, { versionKey: false }); }, { versionKey: false });
HomebrewSchema.statics.increaseView = async function(query) { HomebrewSchema.statics.increaseView = async function(query) {
@@ -43,8 +43,6 @@ HomebrewSchema.statics.increaseView = async function(query) {
return brew; return brew;
}; };
// STATIC FUNCTIONS
HomebrewSchema.statics.get = async function(query, fields=null){ HomebrewSchema.statics.get = async function(query, fields=null){
const brew = await Homebrew.findOne(query, fields).orFail() const brew = await Homebrew.findOne(query, fields).orFail()
.catch((error)=>{throw 'Can not find brew';}); .catch((error)=>{throw 'Can not find brew';});
@@ -65,15 +63,6 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
return brews; return brews;
}; };
// INDEXES
HomebrewSchema.index({ updatedAt: -1, lastViewed: -1 });
HomebrewSchema.index({ published: 1, title: 'text' });
HomebrewSchema.index({ lock: 1, sparse: true });
HomebrewSchema.path('lock.reviewRequested').index({ sparse: true });
const Homebrew = mongoose.model('Homebrew', HomebrewSchema); const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
export { export {

View File

@@ -38,6 +38,15 @@
animation-duration : 0.4s; 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 { //.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right; // background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
//} //}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 B

View File

@@ -1,6 +0,0 @@
{
"name" : "UnearthedArcana",
"renderer" : "V3",
"baseTheme" : false,
"baseSnippets" : false
}

View File

@@ -1,38 +0,0 @@
@import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less';
:root {
//Colors
--HB_Color_Background : #FFFFFF; // White
--HB_Color_WatercolorStain : #000000; // Black
}
.page {
font-family: Cambria,Georgia,serif;
font-size: 14px;
h1, h2, h3, h4 {
font-variant: small-caps;
font-weight: normal;
}
h1 {
column-span: all;
-webkit-column-span: all;
font-size: 40px;
}
h2 {
font-size: 26px;
}
h3 {
font-size: 20px;
border-bottom: 2px solid black;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 16px;
}
h6 {
font-size: 14px;
}
}

View File

@@ -35,13 +35,6 @@
"baseTheme": "Blank", "baseTheme": "Blank",
"baseSnippets": "5ePHB", "baseSnippets": "5ePHB",
"path": "Journal" "path": "Journal"
},
"UnearthedArcana": {
"name": "UnearthedArcana",
"renderer": "V3",
"baseTheme": false,
"baseSnippets": false,
"path": "UnearthedArcana"
} }
} }
} }