0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-27 20:23:08 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
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
38 changed files with 1444 additions and 1481 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" />
@@ -42,7 +40,7 @@ const BrewPage = (props)=>{
...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,7 +21,7 @@ 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,7 +40,10 @@ const Editor = createClass({
style : '' style : ''
}, },
onBrewChange : ()=>{}, onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{}, reportError : ()=>{},
onCursorPageChange : ()=>{}, onCursorPageChange : ()=>{},
@@ -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,14 +1,32 @@
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({
getDefaultProps : function() {
return {
error : '',
parent : null
};
},
render : function() {
const clearError = ()=>{
const state = {
error : null
};
if(this.props.parent.state.isSaving) {
state.isSaving = false;
}
this.props.parent.setState(state);
};
const error = this.props.error;
const response = error.response; const response = error.response;
const errorCode = error.code
const status = response?.status; const status = response?.status;
const errorCode = error.code
const HBErrorCode = response?.body?.HBErrorCode; const HBErrorCode = response?.body?.HBErrorCode;
const message = response?.body?.message; const message = response?.body?.message;
let errMsg = ''; let errMsg = '';
try { try {
errMsg += `${error.toString()}\n\n`; errMsg += `${error.toString()}\n\n`;
@@ -133,6 +151,7 @@ const ErrorNavItem = ({error = '', clearError})=>{
</a>. </a>.
</div> </div>
</Nav.item>; </Nav.item>;
}; }
});
module.exports = ErrorNavItem; module.exports = ErrorNavItem;

View File

@@ -10,40 +10,28 @@ 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 currentNew = localStorage.getItem(BREWKEY);
if(currentNew && !confirm(
`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?`
)) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e)=>{ reader.onload = (e)=>{
const fileContent = e.target.result; const fileContent = e.target.result;
const newBrew = { text: fileContent, style: '' }; const newBrew = {
text : fileContent,
style : ''
};
if(fileContent.startsWith('```metadata')) { if(fileContent.startsWith('```metadata')) {
splitTextStyleAndMetadata(newBrew); splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
localStorage.setItem(BREWKEY, newBrew.text); localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style); localStorage.setItem(STYLEKEY, newBrew.style);
localStorage.setItem(METAKEY, JSON.stringify( localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])
));
window.location.href = '/new'; window.location.href = '/new';
return; } else {
alert('This file is invalid, please, enter a valid file');
} }
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); 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,37 @@
require('./editPage.less');
const React = require('react');
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 BaseEditPage = (props)=>{
return (
<div className={`sitePage ${props.className || ''}`}>
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{props.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{props.navButtons}
<PrintNavItem />
<NewBrewItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem brew={props.brew} storageKey={props.recentStorageKey} />
<AccountNavItem />
</Nav.section>
</Navbar>
{props.children}
</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,516 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import './editPage.less'; require('./editPage.less');
const React = require('react');
import React, { useState, useEffect, useRef, useCallback } from 'react'; const _ = require('lodash');
import request from '../../utils/request-middleware.js'; const createClass = require('create-react-class');
import Markdown from 'naturalcrit/markdown.js'; import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
import _ from 'lodash';;
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm'; import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate'; import { gzipSync, strToU8 } from 'fflate';
import { Meta } from 'vitreum/headtags';
import Nav from 'naturalcrit/nav/nav.jsx'; import request from '../../utils/request-middleware.js';
import Navbar from '../../navbar/navbar.jsx'; const { Meta } = require('vitreum/headtags');
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ShareNavItem from '../../navbar/share.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';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import LockNotification from './lockNotification/lockNotification.jsx'; const ErrorNavItem = require('../../navbar/error-navitem.jsx');
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; const BaseEditPage = require('../basePages/editPage/editPage.jsx');
import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js'; const SplitPane = require('client/components/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const LockNotification = require('./lockNotification/lockNotification.jsx');
import Markdown from 'naturalcrit/markdown.js';
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg'; const googleDriveIcon = require('../../googleDrive.svg');
const SAVE_TIMEOUT = 10000; const SAVE_TIMEOUT = 10000;
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 EditPage = createClass({
displayName : 'EditPage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW_LOAD
};
},
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; getInitialState : function() {
const BREWKEY = 'HB_newPage_content'; return {
const STYLEKEY = 'HB_newPage_style'; brew : this.props.brew,
const SNIPKEY = 'HB_newPage_snippets'; isSaving : false,
const METAKEY = 'HB_newPage_meta'; unsavedChanges : false,
alertTrashedGoogleBrew : this.props.brew.trashed,
alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
error : null,
htmlErrors : Markdown.validate(this.props.brew.text),
url : '',
autoSave : true,
autoSaveWarning : false,
unsavedTime : new Date(),
currentEditorViewPageNum : 1,
currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
displayLockMessage : this.props.brew.lock || false,
themeBundle : {}
};
},
editor : React.createRef(null),
savedBrew : null,
const EditPage = (props)=>{ componentDidMount : function(){
props = { this.setState({
brew : DEFAULT_BREW_LOAD, url : window.location.href
...props });
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
if(this.state.autoSave){
this.trySave();
} else {
this.setState({ autoSaveWarning: true });
}
});
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.unsavedChanges){
return 'You have unsaved changes!';
}
}; };
const [currentBrew , setCurrentBrew ] = useState(props.brew); this.setState((prevState)=>({
const [isSaving , setIsSaving ] = useState(false); htmlErrors : Markdown.validate(prevState.brew.text)
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); fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
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
const useLocalStorage = false; document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount : function() {
window.onbeforeunload = function(){};
document.removeEventListener('keydown', this.handleControlKeys);
},
componentDidUpdate : function(){
const hasChange = this.hasChanges();
if(this.state.unsavedChanges != hasChange){
this.setState({
unsavedChanges : hasChange
});
}
},
useEffect(()=>{ handleControlKeys : function(e){
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.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true); const S_KEY = 83;
if(e.keyCode === 80) printCurrentBrew(); const P_KEY = 80;
if([83, 80].includes(e.keyCode)) { if(e.keyCode == S_KEY) this.trySave(true);
if(e.keyCode == P_KEY) printCurrentBrew();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
} }
}; },
document.addEventListener('keydown', handleControlKeys); handleSplitMove : function(){
window.onbeforeunload = ()=>{ this.editor.current.update();
if(unsavedChangesRef.current) },
return 'You have unsaved changes!';
};
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
window.onBeforeUnload = null;
};
}, []);
useEffect(()=>{ handleEditorViewPageChange : function(pageNumber){
trySaveRef.current = trySave; this.setState({ currentEditorViewPageNum: pageNumber });
unsavedChangesRef.current = unsavedChanges; },
});
useEffect(()=>{ handleEditorCursorPageChange : function(pageNumber){
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); this.setState({ currentEditorCursorPageNum: pageNumber });
setUnsavedChanges(hasChange); },
if(autoSaveEnabled) trySave(false, hasChange); handleBrewRendererPageChange : function(pageNumber){
}, [currentBrew]); this.setState({ currentBrewRendererPageNum: pageNumber });
},
const handleSplitMove = ()=>{ handleTextChange : function(text){
editorRef.current?.update(); //If there are errors, run the validator on every change to give quick feedback
}; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' this.setState((prevState)=>({
if (subfield == 'renderer' || subfield == 'theme') brew : { ...prevState.brew, text: text },
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
//If there are HTML errors, run the validator on every change to give quick feedback handleSnipChange : function(snippet){
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) //If there are errors, run the validator on every change to give quick feedback
setHTMLErrors(Markdown.validate(value)); let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); this.setState((prevState)=>({
else setCurrentBrew(prev => ({ ...prev, [field]: value })); brew : { ...prevState.brew, snippets: snippet },
unsavedChanges : true,
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
if(useLocalStorage) { handleStyleChange : function(style){
if(field == 'text') localStorage.setItem(BREWKEY, value); this.setState((prevState)=>({
if(field == 'style') localStorage.setItem(STYLEKEY, value); brew : { ...prevState.brew, style: style }
if(field == 'snippets') localStorage.setItem(SNIPKEY, value); }), ()=>{if(this.state.autoSave) this.trySave();});
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({ },
renderer : value.renderer,
theme : value.theme, handleMetaChange : function(metadata, field=undefined){
lang : value.lang if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
})); fetchThemeBundle(this, metadata.renderer, metadata.theme);
this.setState((prevState)=>({
brew : {
...prevState.brew,
...metadata
} }
}; }), ()=>{if(this.state.autoSave) this.trySave();});
},
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ hasChanges : function(){
...prevBrew, return !_.isEqual(this.state.brew, this.savedBrew);
},
updateBrew : function(newData){
this.setState((prevState)=>({
brew : {
...prevState.brew,
style : newData.style, style : newData.style,
text : newData.text, text : newData.text,
snippets : newData.snippets snippets : newData.snippets
}
})); }));
},
const resetWarnUnsavedTimer = ()=>{ trySave : function(immediate=false){
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
clearTimeout(warnUnsavedTimeout.current); if(this.state.isSaving)
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings return;
};
const handleGoogleClick = ()=>{ if(immediate) {
if(!global.account?.googleId) { this.debounceSave();
setAlertLoginToTransfer(true); this.debounceSave.flush();
return; return;
} }
setConfirmGoogleTransfer((prev)=>!prev); if(this.hasChanges())
setError(null); this.debounceSave();
}; else
this.debounceSave.cancel();
},
const closeAlerts = (e)=>{ handleGoogleClick : function(){
e.stopPropagation(); //Only handle click once so alert doesn't reopen if(!global.account?.googleId) {
setAlertTrashedGoogleBrew(false); this.setState({
setAlertLoginToTransfer(false); alertLoginToTransfer : true
setConfirmGoogleTransfer(false);
};
const toggleGoogleStorage = ()=>{
setSaveGoogle((prev)=>!prev);
setError(null);
trySave(true);
};
const trySave = (immediate = false, hasChanges = true)=>{
clearTimeout(saveTimeout.current);
if(isSaving) return;
if(!hasChanges && !immediate) return;
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
saveTimeout.current = setTimeout(async ()=>{
setIsSaving(true);
setError(null);
await save(currentBrew, saveGoogle)
.catch((err)=>{
setError(err);
}); });
setIsSaving(false); return;
setLastSavedTime(new Date()); }
if(!autoSaveEnabled) resetWarnUnsavedTimer(); this.setState((prevState)=>({
}, newTimeout); confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}; }));
this.setState({
error : null
});
},
const save = async (brew, saveToGoogle)=>{ closeAlerts : function(event){
setHTMLErrors(Markdown.validate(brew.text)); event.stopPropagation(); //Only handle click once so alert doesn't reopen
this.setState({
alertTrashedGoogleBrew : false,
alertLoginToTransfer : false,
confirmGoogleTransfer : false
});
},
await updateHistory(brew).catch(console.error); toggleGoogleStorage : function(){
this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
error : null
}), ()=>this.trySave(true));
},
save : async function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
const brewState = this.state.brew; // freeze the current state
const preSaveSnapshot = { ...brewState };
this.setState((prevState)=>({
isSaving : true,
error : null,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
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,
googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId, 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,
unsavedTime : new Date()
}), ()=>{
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>
)} <div className='deny'>
No
</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=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a> </a>
<div className='deny'> Not Now </div> <div className='deny'>
Not Now
</div> </div>
)}
{alertTrashedGoogleBrew && (
<div className='errorContainer' onClick={closeAlerts}>
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 />
<div className='confirm'> OK </div>
</div> </div>
)} }
</Nav.item>
); {this.state.alertTrashedGoogleBrew &&
<div className='errorContainer' onClick={this.closeAlerts}>
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 />
<div className='confirm'>
OK
</div>
</div>
}
</Nav.item>;
},
renderSaveButton : function(){
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING // #1 - Currently saving, show SAVING
if(isSaving) if(this.state.isSaving){
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>; return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
}
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
if(unsavedChanges && warnUnsavedChanges) { if(this.state.unsavedChanges && this.state.autoSaveWarning){
resetWarnUnsavedTimer(); this.setAutosaveWarning();
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
const text = elapsedTime === 0 const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
? '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'> return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder... Reminder...
<div className='errorContainer'>{text}</div> <div className='errorContainer'>
{text}
</div>
</Nav.item>; </Nav.item>;
} }
// #3 - Unsaved changes exist, click to save, show SAVE NOW // #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges) // Use trySave(true) instead of save() to use debounced save function
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>; if(this.state.unsavedChanges){
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
}
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled) if(this.state.autoSave){
return <Nav.item className='save saved'>auto-saved.</Nav.item>; return <Nav.item className='save saved'>auto-saved.</Nav.item>;
}
// DEFAULT - No unsaved changes, show SAVED // DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved.</Nav.item>; return <Nav.item className='save saved'>saved.</Nav.item>;
}; },
const toggleAutoSave = ()=>{ handleAutoSave : function(){
clearTimeout(warnUnsavedTimeout.current); if(this.warningTimer) clearTimeout(this.warningTimer);
clearTimeout(saveTimeout.current); this.setState((prevState)=>({
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled)); autoSave : !prevState.autoSave,
setAutoSaveEnabled(!autoSaveEnabled); autoSaveWarning : prevState.autoSave
setWarnUnsavedChanges(autoSaveEnabled); }), ()=>{
}; localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
});
},
const renderAutoSaveButton = ()=>( setAutosaveWarning : function(){
<Nav.item onClick={toggleAutoSave}> setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i> this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
this.warningTimer;
},
errorReported : function(error) {
this.setState({
error
});
},
renderAutoSaveButton : function(){
return <Nav.item onClick={this.handleAutoSave}>
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>;
},
processShareId : function() {
return this.state.brew.googleId && !this.state.brew.stubbed ?
this.state.brew.googleId + this.state.brew.shareId :
this.state.brew.shareId;
},
getRedditLink : function(){
const shareLink = this.processShareId();
const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
const title = `${this.props.brew.title} ${systems}`;
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
},
renderNavbar : function(){
const shareLink = this.processShareId();
return <>
<Nav.section>
{this.renderGoogleDriveIcon()}
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
<Nav.dropdown className='save-menu'>
{this.renderSaveButton()}
{this.renderAutoSaveButton()}
</Nav.dropdown>
}
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item> </Nav.item>
); <Nav.item color='blue' href={`/share/${shareLink}`}>
view
const clearError = ()=>{ </Nav.item>
setError(null); <Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
setIsSaving(false); copy url
}; </Nav.item>
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
const renderNavbar = ()=>{ post to reddit
return <Navbar> </Nav.item>
<Nav.section> </Nav.dropdown>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</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()} navButtons={this.renderNavbar()}
</Nav.dropdown>} recentStorageKey='edit'>
<NewBrewItem/>
<HelpNavItem/>
<ShareNavItem brew={currentBrew} />
<PrintNavItem />
<VaultNavItem />
<RecentNavItem brew={currentBrew} storageKey='edit' />
<AccountNavItem/>
</Nav.section>
</Navbar>;
};
return (
<div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' /> <Meta name='robots' content='noindex, nofollow' />
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
{renderNavbar()}
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
<div className='content'> <div className='content'>
<SplitPane onDragFinish={handleSplitMove}> <SplitPane onDragFinish={this.handleSplitMove}>
<Editor <Editor
ref={editorRef} ref={this.editor}
brew={currentBrew} brew={this.state.brew}
onBrewChange={handleBrewChange} onTextChange={this.handleTextChange}
reportError={setError} onStyleChange={this.handleStyleChange}
renderer={currentBrew.renderer} onSnipChange={this.handleSnipChange}
userThemes={props.userThemes} onMetaChange={this.handleMetaChange}
themeBundle={themeBundle} reportError={this.errorReported}
updateBrew={updateBrew} renderer={this.state.brew.renderer}
onCursorPageChange={setCurrentEditorCursorPageNum} userThemes={this.props.userThemes}
onViewPageChange={setCurrentEditorViewPageNum} themeBundle={this.state.themeBundle}
currentEditorViewPageNum={currentEditorViewPageNum} updateBrew={this.updateBrew}
currentEditorCursorPageNum={currentEditorCursorPageNum} onCursorPageChange={this.handleEditorCursorPageChange}
currentBrewRendererPageNum={currentBrewRendererPageNum} onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={currentBrew.text} text={this.state.brew.text}
style={currentBrew.style} style={this.state.brew.style}
renderer={currentBrew.renderer} renderer={this.state.brew.renderer}
theme={currentBrew.theme} theme={this.state.brew.theme}
themeBundle={themeBundle} themeBundle={this.state.themeBundle}
errors={HTMLErrors} errors={this.state.htmlErrors}
lang={currentBrew.lang} lang={this.state.brew.lang}
onPageChange={setCurrentBrewRendererPageNum} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={currentEditorViewPageNum} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</SplitPane> </SplitPane>
</div> </div>
</div> </BaseEditPage>;
); }
}; });
module.exports = EditPage; module.exports = EditPage;

View File

@@ -1,158 +1,155 @@
import './homePage.less'; require('./homePage.less');
const React = require('react');
import React from 'react'; const createClass = require('create-react-class');
import { useEffect, useState, useRef } from 'react'; const cx = require('classnames');
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js'; const { Meta } = require('vitreum/headtags');
import { Meta } from 'vitreum/headtags';
import Nav from 'naturalcrit/nav/nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import Navbar from '../../navbar/navbar.jsx'; const ErrorNavItem = require('../../navbar/error-navitem.jsx');
import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; const { fetchThemeBundle } = require('../../../../shared/helpers.js');
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import { fetchThemeBundle } 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'; const SplitPane = require('client/components/splitPane/splitPane.jsx');
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const BREWKEY = 'homebrewery-new'; const HomePage = createClass({
const STYLEKEY = 'homebrewery-new-style'; displayName : 'HomePage',
const SNIPKEY = 'homebrewery-new-snippets'; getDefaultProps : function() {
const METAKEY = 'homebrewery-new-meta'; return {
brew : DEFAULT_BREW
const HomePage =(props)=>{
props = {
brew : DEFAULT_BREW,
ver : '0.0.0',
...props
}; };
},
getInitialState : function() {
return {
brew : this.props.brew,
isSaving : false,
welcomeText : this.props.brew.text,
error : undefined,
currentEditorViewPageNum : 1,
currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
themeBundle : {}
};
},
const [currentBrew , setCurrentBrew] = useState(props.brew); editor : React.createRef(null),
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); componentDidMount : function() {
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
},
const useLocalStorage = false; save : function(){
this.setState({
isSaving : true
});
useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
}, []);
const save = ()=>{
request.post('/api') request.post('/api')
.send(currentBrew) .send(this.state.brew)
.end((err, res)=>{ .end((err, res)=>{
if(err) { if(err) {
setError(err); this.setState({ error: err });
return; return;
} }
const saved = res.body; const brew = res.body;
window.location = `/edit/${saved.editId}`; window.location = `/edit/${brew.editId}`;
})
.catch((err)=>{
this.setState({ isSaving: false, error: err });
}); });
}; },
handleSplitMove : function(){
this.editor.current.update();
},
const handleSplitMove = ()=>{ handleEditorViewPageChange : function(pageNumber){
editorRef.current.update(); this.setState({ currentEditorViewPageNum: pageNumber });
}; },
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' handleEditorCursorPageChange : function(pageNumber){
if (subfield == 'renderer' || subfield == 'theme') this.setState({ currentEditorCursorPageNum: pageNumber });
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); },
//If there are HTML errors, run the validator on every change to give quick feedback handleBrewRendererPageChange : function(pageNumber){
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) this.setState({ currentBrewRendererPageNum: pageNumber });
setHTMLErrors(Markdown.validate(value)); },
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); handleTextChange : function(text){
else setCurrentBrew(prev => ({ ...prev, [field]: value })); this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
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
})); }));
},
renderSaveButton : function(){
if(this.state.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={this.save}>
save
</Nav.item>;
} }
}; },
const clearError = ()=>{ renderNavbar : function(){
setError(null); return <>
setIsSaving(false);
};
const renderNavbar = ()=>{
return <Navbar ver={props.ver}>
<Nav.section> <Nav.section>
{error ? {this.state.error ?
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> : <ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
null null
} }
<NewBrewItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section> </Nav.section>
</Navbar>; </>;
}; },
return ( render : function(){
<div className='homePage sitePage'> return <BaseEditPage
className="homePage"
errorState={this.state.error}
parent={this}
brew={this.state.brew}
navButtons={this.renderNavbar()}>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' /> <Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{renderNavbar()}
<div className='content'> <div className='content'>
<SplitPane onDragFinish={handleSplitMove}> <SplitPane onDragFinish={this.handleSplitMove}>
<Editor <Editor
ref={editorRef} ref={this.editor}
brew={currentBrew} brew={this.state.brew}
onBrewChange={handleBrewChange} onTextChange={this.handleTextChange}
renderer={currentBrew.renderer} renderer={this.state.brew.renderer}
showEditButtons={false} showEditButtons={false}
themeBundle={themeBundle} themeBundle={this.state.themeBundle}
onCursorPageChange={setCurrentEditorCursorPageNum} onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={setCurrentEditorViewPageNum} onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={currentEditorViewPageNum} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={currentBrew.text} text={this.state.brew.text}
style={currentBrew.style} style={this.state.brew.style}
renderer={currentBrew.renderer} renderer={this.state.brew.renderer}
onPageChange={setCurrentBrewRendererPageNum} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={currentEditorViewPageNum} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
themeBundle={themeBundle} themeBundle={this.state.themeBundle}
/> />
</SplitPane> </SplitPane>
</div> </div>
<div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}> <div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.save}>
Save current <i className='fas fa-save' /> Save current <i className='fas fa-save' />
</div> </div>
<a href='/new' className='floatingNewButton'> <a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' /> Create your own <i className='fas fa-magic' />
</a> </a>
</div> </BaseEditPage>
) }
}; });
module.exports = HomePage; module.exports = HomePage;

View File

@@ -1,229 +1,266 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
import './newPage.less'; require('./newPage.less');
const React = require('react');
import React, { useState, useEffect, useRef } from 'react'; const createClass = require('create-react-class');
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js'; import Markdown from 'naturalcrit/markdown.js';
import Nav from 'naturalcrit/nav/nav.jsx'; const Nav = require('naturalcrit/nav/nav.jsx');
import Navbar from '../../navbar/navbar.jsx'; const ErrorNavItem = require('../../navbar/error-navitem.jsx');
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
import SplitPane from 'client/components/splitPane/splitPane.jsx'; const BaseEditPage = require('../basePages/editPage/editPage.jsx');
import Editor from '../../editor/editor.jsx'; const SplitPane = require('client/components/splitPane/splitPane.jsx');
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
let SAVEKEY;
const BREWKEY = 'HB_newPage_content'; const NewPage = createClass({
const STYLEKEY = 'HB_newPage_style'; displayName : 'NewPage',
const METAKEY = 'HB_newPage_metadata'; getDefaultProps : function() {
const SNIPKEY = 'HB_newPage_snippets'; return {
const SAVEKEYPREFIX = 'HB_editor_defaultSave_'; brew : DEFAULT_BREW
const NewPage = (props) => {
props = {
brew: DEFAULT_BREW,
...props
}; };
},
const [currentBrew , setCurrentBrew ] = useState(props.brew); getInitialState : function() {
const [isSaving , setIsSaving ] = useState(false); const brew = this.props.brew;
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); return {
brew : brew,
const useLocalStorage = true; isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
useEffect(() => { error : null,
document.addEventListener('keydown', handleControlKeys); htmlErrors : Markdown.validate(brew.text),
loadBrew(); currentEditorViewPageNum : 1,
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
return () => { themeBundle : {}
document.removeEventListener('keydown', handleControlKeys);
}; };
}, []); },
const loadBrew = ()=>{ editor : React.createRef(null),
const brew = { ...currentBrew };
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser componentDidMount : function() {
document.addEventListener('keydown', this.handleControlKeys);
const brew = this.state.brew;
if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
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));
brew.text = brewStorage ?? brew.text; brew.text = brewStorage ?? brew.text;
brew.style = styleStorage ?? brew.style; brew.style = styleStorage ?? brew.style;
// brew.title = metaStorage?.title || this.state.brew.title;
// brew.description = metaStorage?.description || this.state.brew.description;
brew.renderer = metaStorage?.renderer ?? brew.renderer; brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme; brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang; brew.lang = metaStorage?.lang ?? brew.lang;
} }
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`; SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew); this.setState({
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle); brew : brew,
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
});
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
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/');
}; }
},
componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys);
},
const handleControlKeys = (e) => { handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return; if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83; const S_KEY = 83;
const P_KEY = 80; const P_KEY = 80;
if (e.keyCode === S_KEY) save(); if(e.keyCode == S_KEY) this.save();
if (e.keyCode === P_KEY) printCurrentBrew(); if(e.keyCode == P_KEY) printCurrentBrew();
if (e.keyCode === S_KEY || e.keyCode === P_KEY) { if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.preventDefault();
} }
}; },
const handleSplitMove = ()=>{ handleSplitMove : function(){
editorRef.current.update(); this.editor.current.update();
}; },
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata' handleEditorViewPageChange : function(pageNumber){
if (subfield == 'renderer' || subfield == 'theme') this.setState({ currentEditorViewPageNum: pageNumber });
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme); },
//If there are HTML errors, run the validator on every change to give quick feedback handleEditorCursorPageChange : function(pageNumber){
if(HTMLErrors.length && (field == 'text' || field == 'snippets')) this.setState({ currentEditorCursorPageNum: pageNumber });
setHTMLErrors(Markdown.validate(value)); },
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value })); handleBrewRendererPageChange : function(pageNumber){
else setCurrentBrew(prev => ({ ...prev, [field]: value })); this.setState({ currentBrewRendererPageNum: pageNumber });
},
if(useLocalStorage) { handleTextChange : function(text){
if(field == 'text') localStorage.setItem(BREWKEY, value); //If there are errors, run the validator on every change to give quick feedback
if(field == 'style') localStorage.setItem(STYLEKEY, value); let htmlErrors = this.state.htmlErrors;
if(field == 'snippets') localStorage.setItem(SNIPKEY, value); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer, this.setState((prevState)=>({
theme : value.theme, brew : { ...prevState.brew, text: text },
lang : value.lang htmlErrors : htmlErrors,
})); }));
} localStorage.setItem(BREWKEY, text);
}; },
const save = async () => { handleStyleChange : function(style){
setIsSaving(true); this.setState((prevState)=>({
brew : { ...prevState.brew, style: style },
}));
localStorage.setItem(STYLEKEY, style);
},
let updatedBrew = { ...currentBrew }; handleSnipChange : function(snippet){
splitTextStyleAndMetadata(updatedBrew); //If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm; this.setState((prevState)=>({
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1; brew : { ...prevState.brew, snippets: snippet },
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
const res = await request handleMetaChange : function(metadata, field=undefined){
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
.send(updatedBrew) fetchThemeBundle(this, metadata.renderer, metadata.theme);
.catch((err) => {
setIsSaving(false); this.setState((prevState)=>({
setError(err); brew : { ...prevState.brew, ...metadata },
}), ()=>{
localStorage.setItem(METAKEY, JSON.stringify({
// 'title' : this.state.brew.title,
// 'description' : this.state.brew.description,
'renderer' : this.state.brew.renderer,
'theme' : this.state.brew.theme,
'lang' : this.state.brew.lang
}));
});
;
},
save : async function(){
this.setState({
isSaving : true
}); });
setIsSaving(false) let brew = this.state.brew;
// Split out CSS to Style if CSS codefence exists
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
const index = brew.text.indexOf('```\n\n');
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
brew.text = brew.text.slice(index + 5);
}
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
const res = await request
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(brew)
.catch((err)=>{
this.setState({ isSaving: false, error: err });
});
if(!res) return; if(!res) return;
const savedBrew = res.body; brew = res.body;
localStorage.removeItem(BREWKEY); localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY); localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY); localStorage.removeItem(METAKEY);
window.location = `/edit/${savedBrew.editId}`; window.location = `/edit/${brew.editId}`;
}; },
const renderSaveButton = ()=>{ renderSaveButton : function(){
if(isSaving){ if(this.state.isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='save'> return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
save... save...
</Nav.item>; </Nav.item>;
} else { } else {
return <Nav.item icon='fas fa-save' className='save' onClick={save}> return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
save save
</Nav.item>; </Nav.item>;
} }
}; },
const clearError = ()=>{ renderNavbar : function(){
setError(null); return <>
setIsSaving(false);
};
const renderNavbar = () => (
<Navbar>
<Nav.section> <Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item> {this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
this.renderSaveButton()
}
</Nav.section> </Nav.section>
</>;
},
<Nav.section> render : function(){
{error return <BaseEditPage
? <ErrorNavItem error={error} clearError={clearError} /> className="newPage"
: renderSaveButton()} errorState={this.state.error}
<PrintNavItem /> parent={this}
<HelpNavItem /> brew={this.state.brew}
<RecentNavItem /> navButtons={this.renderNavbar()}>
<AccountNavItem />
</Nav.section>
</Navbar>
);
return (
<div className='newPage sitePage'>
{renderNavbar()}
<div className='content'> <div className='content'>
<SplitPane onDragFinish={handleSplitMove}> <SplitPane onDragFinish={this.handleSplitMove}>
<Editor <Editor
ref={editorRef} ref={this.editor}
brew={currentBrew} brew={this.state.brew}
onBrewChange={handleBrewChange} onTextChange={this.handleTextChange}
renderer={currentBrew.renderer} onStyleChange={this.handleStyleChange}
userThemes={props.userThemes} onMetaChange={this.handleMetaChange}
themeBundle={themeBundle} onSnipChange={this.handleSnipChange}
onCursorPageChange={setCurrentEditorCursorPageNum} renderer={this.state.brew.renderer}
onViewPageChange={setCurrentEditorViewPageNum} userThemes={this.props.userThemes}
currentEditorViewPageNum={currentEditorViewPageNum} themeBundle={this.state.themeBundle}
currentEditorCursorPageNum={currentEditorCursorPageNum} onCursorPageChange={this.handleEditorCursorPageChange}
currentBrewRendererPageNum={currentBrewRendererPageNum} onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={currentBrew.text} text={this.state.brew.text}
style={currentBrew.style} style={this.state.brew.style}
renderer={currentBrew.renderer} renderer={this.state.brew.renderer}
theme={currentBrew.theme} theme={this.state.brew.theme}
themeBundle={themeBundle} themeBundle={this.state.themeBundle}
errors={HTMLErrors} errors={this.state.htmlErrors}
lang={currentBrew.lang} lang={this.state.brew.lang}
onPageChange={setCurrentBrewRendererPageNum} onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={currentEditorViewPageNum} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</SplitPane> </SplitPane>
</div> </div>
</div> </BaseEditPage>;
); }
}; });
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,27 +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;
const deleteKeys = storage?.getItem('HB_deleteKeys') != null;
Object.keys(localStorageKeyMap).forEach((key)=>{
if(storage[key]){
if(!storage[localStorageKeyMap[key]]){
const data = storage.getItem(key);
storage.setItem(localStorageKeyMap[key], data);
};
if(deleteKeys) 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

@@ -116,21 +116,27 @@ const printCurrentBrew = ()=>{
} }
}; };
const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ const fetchThemeBundle = async (obj, renderer, theme)=>{
if(!renderer || !theme) return; if(!renderer || !theme) return;
const res = await request const res = await request
.get(`/api/theme/${renderer}/${theme}`) .get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{ .catch((err)=>{
setError(err) obj.setState({ error: err });
}); });
if(!res) { if(!res) {
setThemeBundle({}); obj.setState((prevState)=>({
...prevState,
themeBundle : {}
}));
return; return;
} }
const themeBundle = res.body; const themeBundle = res.body;
themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n'); themeBundle.joinedStyles = themeBundle.styles.map((style)=>`<style>${style}</style>`).join('\n\n');
setThemeBundle(themeBundle); obj.setState((prevState)=>({
setError(null); ...prevState,
themeBundle : themeBundle,
error : null
}));
}; };
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {

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"
} }
} }
} }