0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-24 20:43:03 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Trevor Buckner
698d60e0f2 Adds routes to crawl google users for unsubbed brews 2025-07-28 13:28:48 -04:00
14 changed files with 1194 additions and 1062 deletions

View File

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

View File

@@ -11,8 +11,19 @@ import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
const NewBrew = 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 Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const LockNotification = require('./lockNotification/lockNotification.jsx');
@@ -25,6 +36,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers
const googleDriveIcon = require('../../googleDrive.svg');
const SAVE_TIMEOUT = 10000;
const EditPage = createClass({
displayName : 'EditPage',
@@ -36,17 +48,171 @@ const EditPage = createClass({
getInitialState : function() {
return {
brew : this.props.brew,
isSaving : false,
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 : {}
};
},
componentDidMount : function(){
editor : React.createRef(null),
savedBrew : null,
componentDidMount : function(){
this.setState({
url : window.location.href
});
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!';
}
};
this.setState((prevState)=>({
htmlErrors : Markdown.validate(prevState.brew.text)
}));
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
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
});
}
},
handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
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.preventDefault();
}
},
handleSplitMove : function(){
this.editor.current.update();
},
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
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 : { ...prevState.brew, text: text },
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
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 },
unsavedChanges : true,
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
handleStyleChange : function(style){
this.setState((prevState)=>({
brew : { ...prevState.brew, style: style }
}), ()=>{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
}
}), ()=>{if(this.state.autoSave) this.trySave();});
},
hasChanges : function(){
return !_.isEqual(this.state.brew, this.savedBrew);
},
updateBrew : function(newData){
this.setState((prevState)=>({
brew : {
...prevState.brew,
style : newData.style,
text : newData.text,
snippets : newData.snippets
}
}));
},
trySave : function(immediate=false){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.state.isSaving)
return;
if(immediate) {
this.debounceSave();
this.debounceSave.flush();
return;
}
if(this.hasChanges())
this.debounceSave();
else
this.debounceSave.cancel();
},
handleGoogleClick : function(){
@@ -57,10 +223,11 @@ const EditPage = createClass({
return;
}
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer,
error : null
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}));
this.setState({
error : null
});
},
closeAlerts : function(event){
@@ -136,6 +303,7 @@ const EditPage = createClass({
version : res.body.version
},
isSaving : false,
unsavedTime : new Date()
}), ()=>{
this.setState({ unsavedChanges : this.hasChanges() });
});
@@ -168,7 +336,7 @@ const EditPage = createClass({
You must be signed in to a Google account to transfer
between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.Location.href}`}>
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
@@ -190,6 +358,68 @@ const EditPage = createClass({
</Nav.item>;
},
renderSaveButton : function(){
// #1 - Currently saving, show SAVING
if(this.state.isSaving){
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
}
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
if(this.state.unsavedChanges && this.state.autoSaveWarning){
this.setAutosaveWarning();
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>
{text}
</div>
</Nav.item>;
}
// #3 - Unsaved changes exist, click to save, show SAVE NOW
// Use trySave(true) instead of save() to use debounced save function
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
if(this.state.autoSave){
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
}
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved.</Nav.item>;
},
handleAutoSave : function(){
if(this.warningTimer) clearTimeout(this.warningTimer);
this.setState((prevState)=>({
autoSave : !prevState.autoSave,
autoSaveWarning : prevState.autoSave
}), ()=>{
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
});
},
setAutosaveWarning : function(){
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
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 :
@@ -197,6 +427,7 @@ const EditPage = createClass({
},
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}`;
@@ -210,9 +441,22 @@ const EditPage = createClass({
renderNavbar : function(){
const shareLink = this.processShareId();
return <>
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<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>
}
<NewBrew />
<HelpNavItem/>
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
@@ -227,26 +471,58 @@ const EditPage = createClass({
post to reddit
</Nav.item>
</Nav.dropdown>
<PrintNavItem />
<VaultNavItem />
<RecentNavItem brew={this.state.brew} storageKey='edit' />
<Account />
</Nav.section>
</>;
</Navbar>;
},
render : function(){
return <BaseEditPage
className="editPage"
errorState={this.state.error}
parent={this}
brew={this.state.brew}
renderUniqueNav={this.renderNavbar}
performSave={this.save}
recentStorageKey='edit'>
{(welcomeText, brew, save) => {
return <>
<Meta name='robots' content='noindex, nofollow' />
{this.props.brew.lock && <LockNotification shareId={brew.shareId} message={brew.lock.editMessage} reviewRequested={brew.lock.reviewRequested} />}
</>
}}
</BaseEditPage>;
return <div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
{this.renderNavbar()}
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove}>
<Editor
ref={this.editor}
brew={this.state.brew}
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onSnipChange={this.handleSnipChange}
onMetaChange={this.handleMetaChange}
reportError={this.errorReported}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
themeBundle={this.state.themeBundle}
updateBrew={this.updateBrew}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>;
}
});

View File

@@ -5,9 +5,19 @@ const cx = require('classnames');
import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags');
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 VaultNavItem = require('../../navbar/vault.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
@@ -15,39 +25,116 @@ const HomePage = createClass({
displayName : 'HomePage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW
brew : DEFAULT_BREW,
ver : '0.0.0'
};
},
getInitialState : function() {
return {
brew : this.props.brew,
welcomeText : this.props.brew.text,
error : undefined,
currentEditorViewPageNum : 1,
currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
themeBundle : {}
};
},
save : function(brew){
return request
.post('/api')
.send(brew)
.then((res) => {
const saved = res.body;
window.location = `/edit/${saved.editId}`;
editor : React.createRef(null),
componentDidMount : function() {
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
},
handleSave : function(){
request.post('/api')
.send(this.state.brew)
.end((err, res)=>{
if(err) {
this.setState({ error: err });
return;
}
const brew = res.body;
window.location = `/edit/${brew.editId}`;
});
},
handleSplitMove : function(){
this.editor.current.update();
},
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
}));
},
renderNavbar : function(){
return <Navbar ver={this.props.ver}>
<Nav.section>
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
null
}
<NewBrewItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
},
render : function(){
return <BaseEditPage
{...this.props}
className="homePage"
parent={this}
performSave={this.save}>
{(welcomeText, brew, save) => {
return <>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
<div className={cx('floatingSaveButton', { show: welcomeText != brew.text })} onClick={save}>
Save current <i className='fas fa-save' />
</div>
return <div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove}>
<Editor
ref={this.editor}
brew={this.state.brew}
onTextChange={this.handleTextChange}
renderer={this.state.brew.renderer}
showEditButtons={false}
themeBundle={this.state.themeBundle}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
themeBundle={this.state.themeBundle}
/>
</SplitPane>
</div>
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
Save current <i className='fas fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' />
</a>
</>
}}
</BaseEditPage>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' />
</a>
</div>;
}
});

View File

@@ -4,16 +4,28 @@ const React = require('react');
const createClass = require('create-react-class');
import request from '../../utils/request-middleware.js';
const Nav = require('naturalcrit/nav/nav.jsx');
import Markdown from 'naturalcrit/markdown.js';
const BaseEditPage = require('../basePages/editPage/editPage.jsx');
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');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
let SAVEKEY;
const NewPage = createClass({
displayName : 'NewPage',
@@ -23,24 +35,52 @@ const NewPage = createClass({
};
},
loadBrew : function(brew, setBrew, setSaveGoogle) {
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
//TODO: Move localstorage handling to BaseEditPage?
getInitialState : function() {
const brew = this.props.brew;
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 : {}
};
},
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 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.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.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 saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setBrew(brew);
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle);
this.setState({
brew : brew,
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
});
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
localStorage.setItem(BREWKEY, brew.text);
if(brew.style)
@@ -50,30 +90,185 @@ const NewPage = createClass({
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();
}
},
handleSplitMove : function(){
this.editor.current.update();
},
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
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 : { ...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
}));
});
;
},
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);
}
save : async function(brew, saveGoogle){
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
return request
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
const res = await request
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(brew)
.then((res) => {
//TODO: Move localstorage handling to BaseEditPage?
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY);
const saved = res.body;
window.location = `/edit/${saved.editId}`;
.catch((err)=>{
this.setState({ isSaving: false, error: err });
});
if(!res) return;
brew = res.body;
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY);
window.location = `/edit/${brew.editId}`;
},
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>;
}
},
renderNavbar : function(){
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
this.renderSaveButton()
}
<PrintNavItem />
<HelpNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
},
render : function(){
return <BaseEditPage
{...this.props}
className="newPage"
parent={this}
performSave={this.save}
loadBrew={this.loadBrew}>
</BaseEditPage>;
return <div className='newPage sitePage'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove}>
<Editor
ref={this.editor}
brew={this.state.brew}
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
onSnipChange={this.handleSnipChange}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
themeBundle={this.state.themeBundle}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>;
}
});

View File

@@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
import request from '../../utils/request-middleware.js';

1101
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -142,13 +142,13 @@
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"jest": "^30.0.5",
"jest": "^30.0.4",
"jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^16.22.0",
"stylelint": "^16.21.1",
"stylelint-config-recess-order": "^7.1.0",
"stylelint-config-recommended": "^16.0.0",
"supertest": "^7.1.4"
"supertest": "^7.1.3"
}
}

View File

@@ -161,7 +161,7 @@ fs.emptyDirSync('./build');
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default)
ext : 'js' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed
});
}

View File

@@ -147,6 +147,109 @@ app.get('/', (req, res, next)=>{
return next();
});
app.get('/analyze', async (req, res, next) => {
const accounts = JSON.parse(fs.readFileSync('accounts.json', 'utf8'));
let totalBrewsStubbed = accounts.reduce((sum, account) => {
if (account.brewsStubbed) {
return sum + account.brewsStubbed;
}
return sum;
}, 0);
let totalAccountsProcessed = accounts.filter(account => account.fullyProcessed).length;
let totalAccountsWithInvalidCredentials = accounts.filter(account => account.invalidCredentials).length;
console.log(`Total Brews Stubbed: ${totalBrewsStubbed}`);
console.log(`Total Accounts Processed: ${totalAccountsProcessed}`);
console.log(`Total Accounts with Invalid Credentials: ${totalAccountsWithInvalidCredentials}`);
});
app.get('/destroy', async (req, res, next) => {
const accounts = JSON.parse(fs.readFileSync('accounts.json', 'utf8'));
let updated = false;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
for (let i = 40000; i < accounts.length; i++) {
if (accounts[i].fullyProcessed || accounts[i].invalidCredentials) continue;
const originalAccount = { ...accounts[i] };
const account = accounts[i];
console.log(`Processing account: ${account.username}`);
let googleBrews;
let auth;
try {
auth = await GoogleActions.authCheck(account, res);
googleBrews = await GoogleActions.listGoogleBrews(auth);
} catch (err) {
console.error(`Auth error for ${account.username}`);
accounts[i] = { ...originalAccount, invalidCredentials: true };
updated = true;
continue;
}
if (!auth) {
accounts[i] = { ...originalAccount, missingAuth: true };
updated = true;
continue;
}
console.log('Google Brews:', googleBrews.length);
if (googleBrews.length === 0) {
accounts[i] = { ...originalAccount, fullyProcessed: true, brewsStubbed: 0 };
updated = true;
continue;
}
const fields = ['googleId', 'title', 'editId', 'shareId'];
let brews = await HomebrewModel.getByUser(account.username, true, fields).catch(err => console.error(err));
const stubbedEditIds = new Set(brews.map(b => b.editId));
googleBrews = googleBrews.filter(b => !stubbedEditIds.has(b.editId));
console.log('Unstubbed Google Brews:', googleBrews.length);
const results = await Promise.all(
googleBrews.map(async (brew) => {
let brewFromServer = await GoogleActions.getGoogleBrew(auth, brew.googleId, brew.editId, 'edit');
splitTextStyleAndMetadata(brewFromServer);
brewFromServer.authors = [account.username];
api.excludeStubProps(brewFromServer);
console.log(`Trying to Stub: ${brewFromServer.title} (${brewFromServer.shareId})`);
let saved = await new HomebrewModel(brewFromServer).save().catch(err => console.error(err));
if (saved) {
console.log(`Saved Stub: ${saved.title} (${saved.shareId})`);
return true;
}
return false;
})
);
const stubCount = results.filter(Boolean).length;
console.log('Brews stubbed:', stubCount);
accounts[i] = {
...originalAccount,
brewsStubbed: stubCount,
fullyProcessed: stubCount === googleBrews.length
};
updated = true;
sleep(1000);
}
if (updated) {
fs.writeFileSync('accounts.json', JSON.stringify(accounts, null, 2), 'utf8');
console.log('accounts.json updated');
}
res.send('One account processed');
});
//Home page Legacy
app.get('/legacy', (req, res, next)=>{
req.brew = {

View File

@@ -8,7 +8,8 @@
import Mongoose from 'mongoose';
const getMongoDBURL = (config)=>{
return config.get('mongodb_uri') ||
console.log('mongodb uri', config.get('MONGODB_URI'));
return config.get('MONGODB_URI') ||
config.get('mongolab_uri') ||
'mongodb://127.0.0.1/homebrewery'; // changed from mongodb://localhost/homebrewery to accommodate versions 16+ of node.
};

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