diff --git a/client/homebrew/pages/basePages/editorPage/editorPage.jsx b/client/homebrew/pages/basePages/editorPage/editorPage.jsx
new file mode 100644
index 000000000..99a5c2b52
--- /dev/null
+++ b/client/homebrew/pages/basePages/editorPage/editorPage.jsx
@@ -0,0 +1,578 @@
+/* eslint-disable max-lines */
+require('./editorPage.less');
+const React = require('react');
+const createClass = require('create-react-class');
+const _ = require('lodash');
+const request = require('superagent');
+const { Meta } = require('vitreum/headtags');
+
+const Nav = require('naturalcrit/nav/nav.jsx');
+const Navbar = require('../../../navbar/navbar.jsx');
+
+const NewBrew = require('../../../navbar/newbrew.navitem.jsx');
+const ReportIssue = require('../../../navbar/issue.navitem.jsx');
+const PrintLink = require('../../../navbar/print.navitem.jsx');
+const Account = require('../../../navbar/account.navitem.jsx');
+const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
+
+const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
+const Editor = require('../../../editor/editor.jsx');
+const BrewRenderer = require('../../../brewRenderer/brewRenderer.jsx');
+
+const Markdown = require('naturalcrit/markdown.js');
+
+const googleDriveActive = require('../../../googleDrive.png');
+const googleDriveInactive = require('../../../googleDriveMono.png');
+
+const SAVE_TIMEOUT = 3000;
+
+const BREWKEY = 'homebrewery-new';
+const STYLEKEY = 'homebrewery-new-style';
+const METAKEY = 'homebrewery-new-meta';
+
+const EditorPage = createClass({
+ getDefaultProps : function() {
+ return {
+ brew : {
+ text : '',
+ style : '',
+ shareId : null,
+ editId : null,
+ createdAt : null,
+ updatedAt : null,
+ gDrive : false,
+ trashed : false,
+
+ title : '',
+ description : '',
+ tags : '',
+ published : false,
+ authors : [],
+ systems : [],
+ renderer : 'legacy'
+ },
+ pageType : 'edit',
+ googleDriveOptions : [
+ 'DRIVE > HB',
+ 'HB > DRIVE'
+ ]
+ };
+ },
+
+ getInitialState : function() {
+ return {
+ brew : this.props.brew,
+ isSaving : false,
+ isPending : false,
+ alertTrashedGoogleBrew : this.props.brew.trashed,
+ alertLoginToTransfer : false,
+ saveGoogle : this.props.brew.googleId ? true : false,
+ confirmGoogleTransfer : false,
+ errors : null,
+ htmlErrors : Markdown.validate(this.props.brew.text),
+ url : ''
+ };
+ // return {
+ // brew : {
+ // text : brew.text || '',
+ // style : brew.style || undefined,
+ // gDrive : false,
+ // title : brew.title || '',
+ // description : brew.description || '',
+ // tags : brew.tags || '',
+ // published : false,
+ // authors : [],
+ // systems : brew.systems || [],
+ // renderer : brew.renderer || 'legacy'
+ // },
+
+ // isSaving : false,
+ // isPending : false,
+ // alertTrashedGoogleBrew : this.props.brew.trashed,
+ // alertLoginToTransfer : false,
+ // saveGoogle : (global.account && global.account.googleId ? true : false),
+ // confirmGoogleTransfer : false,
+ // errors : null,
+ // htmlErrors : Markdown.validate(brew.text),
+ // url : ''
+ // };
+ },
+ savedBrew : null,
+
+ componentDidMount : function(){
+ this.setState({
+ url : window.location.href
+ });
+
+ this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
+
+ this.trySave();
+ window.onbeforeunload = ()=>{
+ if(this.state.isSaving || this.state.isPending){
+ return 'You have unsaved changes!';
+ }
+ };
+
+ this.setState((prevState)=>({
+ htmlErrors : Markdown.validate(prevState.brew.text)
+ }));
+
+ document.addEventListener('keydown', this.handleControlKeys);
+ },
+ componentWillUnmount : function() {
+ window.onbeforeunload = 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) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
+ if(e.keyCode == P_KEY || e.keyCode == S_KEY){
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ },
+
+ isEdit : function(){
+ return this.props.pageType == 'edit';
+ },
+
+ isNew : function(){
+ return this.props.pageType == 'new';
+ },
+
+ handleSplitMove : function(){
+ this.refs.editor.update();
+ },
+
+ 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);
+
+ this.setState((prevState)=>({
+ brew : _.merge({}, prevState.brew, { text: text }),
+ isPending : true,
+ htmlErrors : htmlErrors
+ }), ()=>this.trySave());
+ },
+
+ handleStyleChange : function(style){
+ this.setState((prevState)=>({
+ brew : _.merge({}, prevState.brew, { style: style }),
+ isPending : true
+ }), ()=>this.trySave());
+ },
+
+ handleMetaChange : function(metadata){
+ this.setState((prevState)=>({
+ brew : _.merge({}, prevState.brew, metadata),
+ isPending : true,
+ }), ()=>this.trySave());
+
+ },
+
+ hasChanges : function(){
+ return !_.isEqual(this.state.brew, this.savedBrew);
+ },
+
+ trySave : function(){
+ if(!this.isEdit()) return;
+ if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
+ if(this.hasChanges()){
+ this.debounceSave();
+ } else {
+ this.debounceSave.cancel();
+ }
+ },
+
+ handleGoogleClick : function(){
+ if(!global.account?.googleId) {
+ this.setState({
+ alertLoginToTransfer : true
+ });
+ return;
+ }
+ this.setState((prevState)=>({
+ confirmGoogleTransfer : !prevState.confirmGoogleTransfer
+ }));
+ this.clearErrors();
+ },
+
+ closeAlerts : function(event){
+ event.stopPropagation(); //Only handle click once so alert doesn't reopen
+ this.setState({
+ alertTrashedGoogleBrew : false,
+ alertLoginToTransfer : false,
+ confirmGoogleTransfer : false
+ });
+ },
+
+ toggleGoogleStorage : function(){
+ this.setState((prevState)=>({
+ saveGoogle : !prevState.saveGoogle,
+ isSaving : false,
+ errors : null
+ }), ()=>this.isEdit() && this.save());
+ },
+
+ clearErrors : function(){
+ this.setState({
+ errors : null,
+ isSaving : false
+
+ });
+ },
+
+ save : async function(){
+ this.setState((prevState)=>({
+ isSaving : true,
+ errors : null,
+ htmlErrors : Markdown.validate(prevState.brew.text)
+ }));
+
+ if(this.isEdit()){
+ if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
+
+ const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
+
+ const brew = this.state.brew;
+ brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
+
+ if(this.state.saveGoogle) {
+ if(transfer) {
+ const res = await request
+ .post('/api/newGoogle/')
+ .send(brew)
+ .catch((err)=>{
+ console.log(err.status === 401
+ ? 'Not signed in!'
+ : 'Error Transferring to Google!');
+ this.setState({ errors: err, saveGoogle: false });
+ });
+
+ if(!res) { return; }
+
+ console.log('Deleting Local Copy');
+ await request.delete(`/api/${brew.editId}`)
+ .send()
+ .catch((err)=>{
+ console.log('Error deleting Local Copy');
+ });
+
+ this.savedBrew = res.body;
+ history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
+ } else {
+ const res = await request
+ .put(`/api/updateGoogle/${brew.editId}`)
+ .send(brew)
+ .catch((err)=>{
+ console.log(err.status === 401
+ ? 'Not signed in!'
+ : 'Error Saving to Google!');
+ this.setState({ errors: err });
+ return;
+ });
+
+ this.savedBrew = res.body;
+ }
+ } else {
+ if(transfer) {
+ const res = await request.post('/api')
+ .send(brew)
+ .catch((err)=>{
+ console.log('Error creating Local Copy');
+ this.setState({ errors: err });
+ return;
+ });
+
+ await request.get(`/api/removeGoogle/${brew.googleId}${brew.editId}`)
+ .send()
+ .catch((err)=>{
+ console.log('Error Deleting Google Brew');
+ });
+
+ this.savedBrew = res.body;
+ history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
+ } else {
+ const res = await request
+ .put(`/api/update/${brew.editId}`)
+ .send(brew)
+ .catch((err)=>{
+ console.log('Error Updating Local Brew');
+ this.setState({ errors: err });
+ return;
+ });
+
+ this.savedBrew = res.body;
+ }
+ }
+
+ this.setState((prevState)=>({
+ brew : _.merge({}, prevState.brew, {
+ googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
+ editId : this.savedBrew.editId,
+ shareId : this.savedBrew.shareId
+ }),
+ isPending : false,
+ isSaving : false,
+ }));
+ }
+
+ if(this.isNew()){
+ console.log('saving new brew');
+
+ 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;
+
+ if(this.state.saveGoogle) {
+ const res = await request
+ .post('/api/newGoogle/')
+ .send(brew)
+ .catch((err)=>{
+ console.log(err.status === 401
+ ? 'Not signed in!'
+ : 'Error Creating New Google Brew!');
+ this.setState({ isSaving: false, errors: err });
+ return;
+ });
+
+ brew = res.body;
+ localStorage.removeItem(BREWKEY);
+ localStorage.removeItem(STYLEKEY);
+ localStorage.removeItem(METAKEY);
+ window.location = `/edit/${brew.googleId}${brew.editId}`;
+ } else {
+ request.post('/api')
+ .send(brew)
+ .end((err, res)=>{
+ if(err){
+ this.setState({
+ isSaving : false
+ });
+ return;
+ }
+ window.onbeforeunload = function(){};
+ brew = res.body;
+ localStorage.removeItem(BREWKEY);
+ localStorage.removeItem(STYLEKEY);
+ localStorage.removeItem(METAKEY);
+ window.location = `/edit/${brew.editId}`;
+ });
+ }
+ }
+ },
+
+ renderGoogleDriveIcon : function(){
+ return
+ :
+ }
+
+ {this.state.confirmGoogleTransfer &&
+
+
Google Drive!
+
+
+ Report the issue
+ here
+ .
+
If you want to keep it, make sure to move it before it is deleted permanently!
+