+ return (
+
- {this.renderNavbar()}
+ {renderNavbar()}
-
+
-
;
- }
-});
+
+ );
+};
module.exports = HomePage;
diff --git a/client/homebrew/pages/homePage/homePage.less b/client/homebrew/pages/homePage/homePage.less
index 4cf9ff4fe..b89c2c7dd 100644
--- a/client/homebrew/pages/homePage/homePage.less
+++ b/client/homebrew/pages/homePage/homePage.less
@@ -35,6 +35,14 @@
.navItem.save {
background-color : @orange;
+ transition:all 0.2s;
&:hover { background-color : @green; }
+
+ &.neverSaved {
+ translate:-100%;
+ opacity: 0;
+ background-color :#333;
+ cursor:auto;
+ }
}
}
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx
index ab7c22541..13bddfc87 100644
--- a/client/homebrew/pages/newPage/newPage.jsx
+++ b/client/homebrew/pages/newPage/newPage.jsx
@@ -1,275 +1,270 @@
-/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
-require('./newPage.less');
-const React = require('react');
-const createClass = require('create-react-class');
-import request from '../../utils/request-middleware.js';
+/* eslint-disable max-lines */
+import './newPage.less';
-import Markdown from 'naturalcrit/markdown.js';
+// Common imports
+import React, { useState, useEffect, useRef } from 'react';
+import request from '../../utils/request-middleware.js';
+import Markdown from 'markdown.js';
+import _ from 'lodash';
-const Nav = require('naturalcrit/nav/nav.jsx');
-const PrintNavItem = require('../../navbar/print.navitem.jsx');
-const Navbar = require('../../navbar/navbar.jsx');
-const AccountNavItem = require('../../navbar/account.navitem.jsx');
-const ErrorNavItem = require('../../navbar/error-navitem.jsx');
-const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
-const HelpNavItem = require('../../navbar/help.navitem.jsx');
+import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
+import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
-const SplitPane = require('client/components/splitPane/splitPane.jsx');
-const Editor = require('../../editor/editor.jsx');
-const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
+import SplitPane from 'client/components/splitPane/splitPane.jsx';
+import Editor from '../../editor/editor.jsx';
+import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
-const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
-const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
+import Nav from 'naturalcrit/nav/nav.jsx';
+import Navbar from '../../navbar/navbar.jsx';
+import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
+import AccountNavItem from '../../navbar/account.navitem.jsx';
+import ErrorNavItem from '../../navbar/error-navitem.jsx';
+import HelpNavItem from '../../navbar/help.navitem.jsx';
+import VaultNavItem from '../../navbar/vault.navitem.jsx';
+import PrintNavItem from '../../navbar/print.navitem.jsx';
+import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
-const BREWKEY = 'homebrewery-new';
-const STYLEKEY = 'homebrewery-new-style';
-const METAKEY = 'homebrewery-new-meta';
-let SAVEKEY;
+// Page specific imports
+import { Meta } from 'vitreum/headtags';
+const BREWKEY = 'HB_newPage_content';
+const STYLEKEY = 'HB_newPage_style';
+const METAKEY = 'HB_newPage_metadata';
+const SNIPKEY = 'HB_newPage_snippets';
+const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
-const NewPage = createClass({
- displayName : 'NewPage',
- getDefaultProps : function() {
- return {
- brew : DEFAULT_BREW
+const useLocalStorage = true;
+const neverSaved = true;
+
+const NewPage = (props)=>{
+ props = {
+ brew : DEFAULT_BREW,
+ ...props
+ };
+
+ const [currentBrew , setCurrentBrew ] = useState(props.brew);
+ const [isSaving , setIsSaving ] = useState(false);
+ const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
+ const [error , setError ] = useState(null);
+ const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
+ const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
+ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
+ const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
+ const [themeBundle , setThemeBundle ] = useState({});
+ const [unsavedChanges , setUnsavedChanges ] = useState(false);
+ const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
+
+ const editorRef = useRef(null);
+ const lastSavedBrew = useRef(_.cloneDeep(props.brew));
+
+ useEffect(()=>{
+ loadBrew();
+ fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
+
+ const handleControlKeys = (e)=>{
+ if(!(e.ctrlKey || e.metaKey)) return;
+ if(e.keyCode === 83) trySaveRef.current(true);
+ if(e.keyCode === 80) printCurrentBrew();
+ if([83, 80].includes(e.keyCode)) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
};
- },
- getInitialState : function() {
- const brew = this.props.brew;
+ document.addEventListener('keydown', handleControlKeys);
- return {
- brew : brew,
- isSaving : false,
- saveGoogle : (global.account && global.account.googleId ? true : false),
- error : null,
- htmlErrors : Markdown.validate(brew.text),
- currentEditorViewPageNum : 1,
- currentEditorCursorPageNum : 1,
- currentBrewRendererPageNum : 1,
- themeBundle : {}
+ return ()=>{
+ document.removeEventListener('keydown', handleControlKeys);
};
- },
+ }, []);
- editor : React.createRef(null),
-
- 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 loadBrew = ()=>{
+ const brew = { ...currentBrew };
+ if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
const brewStorage = localStorage.getItem(BREWKEY);
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.style = styleStorage ?? brew.style;
- // brew.title = metaStorage?.title || this.state.brew.title;
- // brew.description = metaStorage?.description || this.state.brew.description;
+ brew.text = brewStorage ?? brew.text;
+ brew.style = styleStorage ?? brew.style;
brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang;
}
- SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
+ const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
- this.setState({
- brew : brew,
- saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
- });
-
- fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
+ setCurrentBrew(brew);
+ lastSavedBrew.current = brew;
+ setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
localStorage.setItem(BREWKEY, brew.text);
if(brew.style)
localStorage.setItem(STYLEKEY, brew.style);
- localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
- if(window.location.pathname != '/new') {
+ localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang }));
+ if(window.location.pathname !== '/new')
window.history.replaceState({}, window.location.title, '/new/');
- }
- },
- componentWillUnmount : function() {
- document.removeEventListener('keydown', this.handleControlKeys);
- },
+ };
- handleControlKeys : function(e){
- if(!(e.ctrlKey || e.metaKey)) return;
- const S_KEY = 83;
- const P_KEY = 80;
- if(e.keyCode == S_KEY) this.save();
- if(e.keyCode == P_KEY) printCurrentBrew();
- if(e.keyCode == P_KEY || e.keyCode == S_KEY){
- e.stopPropagation();
- e.preventDefault();
- }
- },
+ useEffect(()=>{
+ const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
+ setUnsavedChanges(hasChange);
- handleSplitMove : function(){
- this.editor.current.update();
- },
+ if(autoSaveEnabled) trySave(false, hasChange);
+ }, [currentBrew]);
- handleEditorViewPageChange : function(pageNumber){
- this.setState({ currentEditorViewPageNum: pageNumber });
- },
+ const handleSplitMove = ()=>{
+ editorRef.current.update();
+ };
- handleEditorCursorPageChange : function(pageNumber){
- this.setState({ currentEditorCursorPageNum: pageNumber });
- },
+ const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
+ if(subfield == 'renderer' || subfield == 'theme')
+ fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
- handleBrewRendererPageChange : function(pageNumber){
- this.setState({ currentBrewRendererPageNum: pageNumber });
- },
+ //If there are HTML errors, run the validator on every change to give quick feedback
+ if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
+ setHTMLErrors(Markdown.validate(value));
- handleTextChange : function(text){
- //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);
+ if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
+ else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
- this.setState((prevState)=>({
- brew : { ...prevState.brew, text: text },
- htmlErrors : htmlErrors,
- }));
- localStorage.setItem(BREWKEY, text);
- },
-
- handleStyleChange : function(style){
- this.setState((prevState)=>({
- brew : { ...prevState.brew, style: style },
- }));
- localStorage.setItem(STYLEKEY, style);
- },
-
- handleSnipChange : function(snippet){
- //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);
-
- this.setState((prevState)=>({
- brew : { ...prevState.brew, snippets: snippet },
- htmlErrors : htmlErrors,
- }), ()=>{if(this.state.autoSave) this.trySave();});
- },
-
- handleMetaChange : function(metadata, field=undefined){
- 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 },
- }), ()=>{
- 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
+ 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
}));
- });
- ;
- },
-
- save : async function(){
- this.setState({
- isSaving : true
- });
-
- 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);
}
+ };
+
+ const save = async ()=>{
+ setIsSaving(true);
+
+ const updatedBrew = { ...currentBrew };
+ splitTextStyleAndMetadata(updatedBrew);
+
+ const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
+ updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
- 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)
+ .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
+ .send(updatedBrew)
.catch((err)=>{
- this.setState({ isSaving: false, error: err });
+ setIsSaving(false);
+ setError(err);
});
+
+ setIsSaving(false);
if(!res) return;
- brew = res.body;
+ const savedBrew = res.body;
+
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY);
- window.location = `/edit/${brew.editId}`;
- },
+ window.location = `/edit/${savedBrew.editId}`;
+ };
- renderSaveButton : function(){
- if(this.state.isSaving){
- return
- save...
- ;
- } else {
- return
- save
- ;
- }
- },
+ const renderSaveButton = ()=>{
+ // #1 - Currently saving, show SAVING
+ if(isSaving)
+ return
saving...;
- renderNavbar : function(){
- return
+ // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
+ // if(unsavedChanges && warnUnsavedChanges) {
+ // resetWarnUnsavedTimer();
+ // const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
+ // const text = elapsedTime === 0
+ // ? 'Autosave is OFF.'
+ // : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
+ // return
+ // Reminder...
+ // {text}
+ // ;
+ // }
+
+ // #3 - Unsaved changes exist, click to save, show SAVE NOW
+ if(unsavedChanges)
+ return save now;
+
+ // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
+ if(autoSaveEnabled)
+ return auto-saved;
+
+ // #5 - No unsaved changes, and has never been saved, hide the button
+ if(neverSaved)
+ return save now;
+
+ // DEFAULT - No unsaved changes, show SAVED
+ return saved;
+ };
+
+ const clearError = ()=>{
+ setError(null);
+ setIsSaving(false);
+ };
+
+ const renderNavbar = ()=>(
+
- {this.state.brew.title}
+ {currentBrew.title}
- {this.state.error ?
- :
- this.renderSaveButton()
- }
+ {error
+ ?
+ : renderSaveButton()}
+
+
- ;
- },
+
+ );
- render : function(){
- return
- {this.renderNavbar()}
+ return (
+
;
- }
-});
+
+ );
+};
module.exports = NewPage;
diff --git a/client/homebrew/pages/newPage/newPage.less b/client/homebrew/pages/newPage/newPage.less
index ebc44d543..083e1ee09 100644
--- a/client/homebrew/pages/newPage/newPage.less
+++ b/client/homebrew/pages/newPage/newPage.less
@@ -1,6 +1,12 @@
.newPage {
.navItem.save {
+ .fadeInRight();
+ .transition(opacity);
background-color : @orange;
&:hover { background-color : @green; }
+ &.neverSaved {
+ .fadeOutRight();
+ opacity: 0;
+ }
}
}
diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx
index e9c5540a2..50104a665 100644
--- a/client/homebrew/pages/sharePage/sharePage.jsx
+++ b/client/homebrew/pages/sharePage/sharePage.jsx
@@ -17,15 +17,11 @@ const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpe
const SharePage = (props)=>{
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
- const [state, setState] = useState({
- themeBundle : {},
- currentBrewRendererPageNum : 1,
- });
+ const [themeBundle, setThemeBundle] = useState({});
+ const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const handleBrewRendererPageChange = useCallback((pageNumber)=>{
- setState((prevState)=>({
- currentBrewRendererPageNum : pageNumber,
- ...prevState }));
+ setCurrentBrewRendererPageNum(pageNumber);
}, []);
const handleControlKeys = (e)=>{
@@ -40,11 +36,7 @@ const SharePage = (props)=>{
useEffect(()=>{
document.addEventListener('keydown', handleControlKeys);
- fetchThemeBundle(
- { setState },
- brew.renderer,
- brew.theme
- );
+ fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
@@ -114,9 +106,9 @@ const SharePage = (props)=>{
lang={brew.lang}
renderer={brew.renderer}
theme={brew.theme}
- themeBundle={state.themeBundle}
+ themeBundle={themeBundle}
onPageChange={handleBrewRendererPageChange}
- currentBrewRendererPageNum={state.currentBrewRendererPageNum}
+ currentBrewRendererPageNum={currentBrewRendererPageNum}
allowPrint={true}
/>
diff --git a/client/homebrew/pages/userPage/userPage.jsx b/client/homebrew/pages/userPage/userPage.jsx
index f6fae639d..e4a8b0b4e 100644
--- a/client/homebrew/pages/userPage/userPage.jsx
+++ b/client/homebrew/pages/userPage/userPage.jsx
@@ -39,10 +39,14 @@ const UserPage = (props)=>{
}] : [])
];
+ const clearError = ()=>{
+ setError(null);
+ };
+
const navItems = (