0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-27 11:43:09 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
a0dc23385d Bump codemirror from 5.65.17 to 6.0.2
Bumps [codemirror](https://github.com/codemirror/basic-setup) from 5.65.17 to 6.0.2.
- [Changelog](https://github.com/codemirror/basic-setup/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/basic-setup/commits/6.0.2)

---
updated-dependencies:
- dependency-name: codemirror
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-15 15:20:33 +00:00
20 changed files with 1326 additions and 1384 deletions

View File

@@ -20,8 +20,6 @@ import HeaderNav from './headerNav/headerNav.jsx';
import { safeHTML } from './safeHTML.js'; import { safeHTML } from './safeHTML.js';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const INITIAL_CONTENT = dedent` const INITIAL_CONTENT = dedent`
@@ -39,8 +37,8 @@ const BrewPage = (props)=>{
index : 0, index : 0,
...props ...props
}; };
const pageRef = useRef(null); const pageRef = useRef(null);
const cleanText = safeHTML(props.contents); const cleanText = safeHTML(`${props.contents}\n<div class="columnSplit"></div>\n`);
useEffect(()=>{ useEffect(()=>{
if(!pageRef.current) return; if(!pageRef.current) return;
@@ -132,7 +130,7 @@ const BrewRenderer = (props)=>{
const pagesRef = useRef(null); const pagesRef = useRef(null);
if(props.renderer == 'legacy') { if(props.renderer == 'legacy') {
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY); rawPages = props.text.split('\\page');
} else { } else {
rawPages = props.text.split(PAGEBREAK_REGEX_V3); rawPages = props.text.split(PAGEBREAK_REGEX_V3);
} }
@@ -189,7 +187,6 @@ const BrewRenderer = (props)=>{
let attributes = {}; let attributes = {};
if(props.renderer == 'legacy') { if(props.renderer == 'legacy') {
pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
const html = MarkdownLegacy.render(pageText); const html = MarkdownLegacy.render(pageText);
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />; return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;

View File

@@ -1,75 +1,95 @@
/* eslint-disable camelcase */ //╔===--------------- Polyfills --------------===╗//
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers import 'core-js/es/string/to-well-formed.js';
import './homebrew.less'; //╚===--------------- ---------------===╝//
import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
import HomePage from './pages/homePage/homePage.jsx'; require('./homebrew.less');
import EditPage from './pages/editPage/editPage.jsx'; const React = require('react');
import UserPage from './pages/userPage/userPage.jsx'; const createClass = require('create-react-class');
import SharePage from './pages/sharePage/sharePage.jsx'; const { StaticRouter:Router } = require('react-router');
import NewPage from './pages/newPage/newPage.jsx'; const { Route, Routes, useParams, useSearchParams } = require('react-router');
import ErrorPage from './pages/errorPage/errorPage.jsx';
import VaultPage from './pages/vaultPage/vaultPage.jsx';
import AccountPage from './pages/accountPage/accountPage.jsx';
const WithRoute = ({ el: Element, ...rest })=>{ const HomePage = require('./pages/homePage/homePage.jsx');
const EditPage = require('./pages/editPage/editPage.jsx');
const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx');
const WithRoute = (props)=>{
const params = useParams(); const params = useParams();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const queryParams = Object.fromEntries(searchParams?.entries() || []); const queryParams = {};
for (const [key, value] of searchParams?.entries() || []) {
return <Element {...rest} {...params} query={queryParams} />; queryParams[key] = value;
}
const Element = props.el;
const allProps = {
...props,
...params,
query : queryParams,
el : undefined
};
return <Element {...allProps} />;
}; };
const Homebrew = (props)=>{ const Homebrew = createClass({
const { displayName : 'Homebrewery',
url = '', getDefaultProps : function() {
version = '0.0.0', return {
account = null, url : '',
enable_v3 = false, welcomeText : '',
enable_themes, changelog : '',
config, version : '0.0.0',
brew = { account : null,
title : '', enable_v3 : false,
text : '', brew : {
shareId : null, title : '',
editId : null, text : '',
createdAt : null, shareId : null,
updatedAt : null, editId : null,
lang : '' createdAt : null,
}, updatedAt : null,
userThemes, lang : ''
brews }
} = props; };
},
global.account = account; getInitialState : function() {
global.version = version; global.account = this.props.account;
global.enable_v3 = enable_v3; global.version = this.props.version;
global.enable_themes = enable_themes; global.enable_v3 = this.props.enable_v3;
global.config = config; global.enable_themes = this.props.enable_themes;
global.config = this.props.config;
return ( return {};
<Router location={url}> },
<div className='homebrew'>
<Routes> render : function (){
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} /> return (
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} /> <Router location={this.props.url}>
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} /> <div className='homebrew'>
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } /> <Routes>
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} /> <Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/> <Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} /> <Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} /> <Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} /> <Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} /> <Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} /> <Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} /> <Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} /> <Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} /> <Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
</Routes> <Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
</div> <Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
</Router> <Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
); <Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
}; </Routes>
</div>
</Router>
);
}
});
module.exports = Homebrew; module.exports = Homebrew;

View File

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

View File

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

View File

@@ -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

@@ -21,7 +21,7 @@ const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../navbar/vault.navitem.jsx'); const VaultNavItem = require('../../navbar/vault.navitem.jsx');
const SplitPane = require('client/components/splitPane/splitPane.jsx'); const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
@@ -97,7 +97,7 @@ const EditPage = createClass({
htmlErrors : Markdown.validate(prevState.brew.text) htmlErrors : Markdown.validate(prevState.brew.text)
})); }));
fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
@@ -173,7 +173,7 @@ const EditPage = createClass({
handleMetaChange : function(metadata, field=undefined){ handleMetaChange : function(metadata, field=undefined){
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme); fetchThemeBundle(this, metadata.renderer, metadata.theme);
this.setState((prevState)=>({ this.setState((prevState)=>({
brew : { brew : {
@@ -266,7 +266,7 @@ const EditPage = createClass({
brew.text = brew.text.normalize('NFC'); brew.text = brew.text.normalize('NFC');
this.savedBrew.text = this.savedBrew.text.normalize('NFC'); this.savedBrew.text = this.savedBrew.text.normalize('NFC');
brew.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;
brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text))); brew.patches = stringifyPatches(makePatches(this.savedBrew.text, brew.text));
brew.hash = await md5(this.savedBrew.text); brew.hash = await md5(this.savedBrew.text);
//brew.text = undefined; - Temporary parallel path //brew.text = undefined; - Temporary parallel path
brew.textBin = undefined; brew.textBin = undefined;
@@ -438,13 +438,6 @@ const EditPage = createClass({
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
}, },
clearError : function(){
setState({
error : null,
isSaving : false
})
},
renderNavbar : function(){ renderNavbar : function(){
const shareLink = this.processShareId(); const shareLink = this.processShareId();
@@ -456,7 +449,7 @@ const EditPage = createClass({
<Nav.section> <Nav.section>
{this.renderGoogleDriveIcon()} {this.renderGoogleDriveIcon()}
{this.state.error ? {this.state.error ?
<ErrorNavItem error={this.state.error} clearError={this.clearError}></ErrorNavItem> : <ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
<Nav.dropdown className='save-menu'> <Nav.dropdown className='save-menu'>
{this.renderSaveButton()} {this.renderSaveButton()}
{this.renderAutoSaveButton()} {this.renderAutoSaveButton()}

View File

@@ -1,91 +1,90 @@
import './homePage.less'; require('./homePage.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags');
import React from 'react'; const Nav = require('naturalcrit/nav/nav.jsx');
import { useEffect, useState, useRef } from 'react'; const Navbar = require('../../navbar/navbar.jsx');
import request from '../../utils/request-middleware.js'; const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
import { Meta } from 'vitreum/headtags'; 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');
import Nav from 'naturalcrit/nav/nav.jsx'; const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
import Navbar from '../../navbar/navbar.jsx'; const Editor = require('../../editor/editor.jsx');
import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
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 { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; const HomePage = createClass({
displayName : 'HomePage',
getDefaultProps : function() {
return {
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 : {}
};
},
const HomePage =(props)=>{ editor : React.createRef(null),
props = {
brew : DEFAULT_BREW,
ver : '0.0.0',
...props
};
const [brew , setBrew] = useState(props.brew); componentDidMount : function() {
const [welcomeText , setWelcomeText] = useState(props.brew.text); fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
const [error , setError] = useState(undefined); },
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); handleSave : function(){
useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme);
}, []);
const save = ()=>{
request.post('/api') request.post('/api')
.send(brew) .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}`;
}); });
}; },
handleSplitMove : function(){
this.editor.current.update();
},
const handleSplitMove = ()=>{ handleEditorViewPageChange : function(pageNumber){
editorRef.current.update(); this.setState({ currentEditorViewPageNum: pageNumber });
}; },
const handleEditorViewPageChange = (pageNumber)=>{ handleEditorCursorPageChange : function(pageNumber){
setCurrentEditorViewPageNum(pageNumber); this.setState({ currentEditorCursorPageNum: pageNumber });
}; },
const handleEditorCursorPageChange = (pageNumber)=>{ handleBrewRendererPageChange : function(pageNumber){
setCurrentEditorCursorPageNum(pageNumber); this.setState({ currentBrewRendererPageNum: pageNumber });
}; },
const handleBrewRendererPageChange = (pageNumber)=>{ handleTextChange : function(text){
setCurrentBrewRendererPageNum(pageNumber); this.setState((prevState)=>({
}; brew : { ...prevState.brew, text: text },
}));
const handleTextChange = (text)=>{ },
setBrew((prevBrew) => ({ ...prevBrew, text })); renderNavbar : function(){
}; return <Navbar ver={this.props.ver}>
const clearError = ()=>{
setError(null);
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 /> <NewBrewItem />
@@ -95,48 +94,48 @@ const HomePage =(props)=>{
<AccountNavItem /> <AccountNavItem />
</Nav.section> </Nav.section>
</Navbar>; </Navbar>;
}; },
return ( render : function(){
<div className='homePage sitePage'> return <div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' /> <Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{renderNavbar()} {this.renderNavbar()}
<div className='content'> <div className='content'>
<SplitPane onDragFinish={handleSplitMove}> <SplitPane onDragFinish={this.handleSplitMove}>
<Editor <Editor
ref={editorRef} ref={this.editor}
brew={brew} brew={this.state.brew}
onTextChange={handleTextChange} onTextChange={this.handleTextChange}
renderer={brew.renderer} renderer={this.state.brew.renderer}
showEditButtons={false} showEditButtons={false}
themeBundle={themeBundle} themeBundle={this.state.themeBundle}
onCursorPageChange={handleEditorCursorPageChange} onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange} onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={currentEditorViewPageNum} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
/> />
<BrewRenderer <BrewRenderer
text={brew.text} text={this.state.brew.text}
style={brew.style} style={this.state.brew.style}
renderer={brew.renderer} renderer={this.state.brew.renderer}
onPageChange={handleBrewRendererPageChange} 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 !== brew.text ? ' show' : ''}`} onClick={save}> <div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
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> </div>;
) }
}; });
module.exports = HomePage; module.exports = HomePage;

View File

@@ -1,251 +1,275 @@
/*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');
const createClass = require('create-react-class');
import request from '../../utils/request-middleware.js';
import React, { useState, useEffect, useRef } from 'react'; import Markdown from 'naturalcrit/markdown.js';
import request from '../../utils/request-middleware.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 PrintNavItem = require('../../navbar/print.navitem.jsx');
import AccountNavItem from '../../navbar/account.navitem.jsx'; const Navbar = require('../../navbar/navbar.jsx');
import ErrorNavItem from '../../navbar/error-navitem.jsx'; const AccountNavItem = require('../../navbar/account.navitem.jsx');
import HelpNavItem from '../../navbar/help.navitem.jsx'; const ErrorNavItem = require('../../navbar/error-navitem.jsx');
import PrintNavItem from '../../navbar/print.navitem.jsx'; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; const HelpNavItem = require('../../navbar/help.navitem.jsx');
import SplitPane from 'client/components/splitPane/splitPane.jsx'; const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
import Editor from '../../editor/editor.jsx'; const Editor = require('../../editor/editor.jsx');
import BrewRenderer from '../../brewRenderer/brewRenderer.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 BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style'; const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta'; const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; let SAVEKEY;
const NewPage = (props)=>{
props = {
brew : DEFAULT_BREW,
...props
};
const [currentBrew , setCurrentBrew ] = useState(props.brew); const NewPage = createClass({
const [isSaving , setIsSaving ] = useState(false); displayName : 'NewPage',
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false); getDefaultProps : function() {
const [error , setError ] = useState(null); return {
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); brew : DEFAULT_BREW
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const editorRef = useRef(null);
useEffect(()=>{
document.addEventListener('keydown', handleControlKeys);
loadBrew();
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
}; };
}, []); },
const loadBrew = ()=>{ getInitialState : function() {
const brew = { ...currentBrew }; const brew = this.props.brew;
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
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 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;
} }
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 handleEditorViewPageChange = (pageNumber)=>{ handleEditorViewPageChange : function(pageNumber){
setCurrentEditorViewPageNum(pageNumber); this.setState({ currentEditorViewPageNum: pageNumber });
}; },
const handleEditorCursorPageChange = (pageNumber)=>{ handleEditorCursorPageChange : function(pageNumber){
setCurrentEditorCursorPageNum(pageNumber); this.setState({ currentEditorCursorPageNum: pageNumber });
}; },
const handleBrewRendererPageChange = (pageNumber)=>{ handleBrewRendererPageChange : function(pageNumber){
setCurrentBrewRendererPageNum(pageNumber); this.setState({ currentBrewRendererPageNum: pageNumber });
}; },
const handleTextChange = (text)=>{ handleTextChange : function(text){
//If there are HTML errors, run the validator on every change to give quick feedback //If there are errors, run the validator on every change to give quick feedback
if(HTMLErrors.length) let htmlErrors = this.state.htmlErrors;
HTMLErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
setHTMLErrors(HTMLErrors); this.setState((prevState)=>({
setCurrentBrew((prevBrew)=>({ ...prevBrew, text })); brew : { ...prevState.brew, text: text },
localStorage.setItem(BREWKEY, text); htmlErrors : htmlErrors,
};
const handleStyleChange = (style)=>{
setCurrentBrew((prevBrew)=>({ ...prevBrew, style }));
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(snippet);
setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet }));
};
const handleMetaChange = (metadata, field = undefined)=>{
if(field === 'theme' || field === 'renderer')
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setCurrentBrew((prev)=>({ ...prev, ...metadata }));
localStorage.setItem(METAKEY, JSON.stringify({
renderer : metadata.renderer,
theme : metadata.theme,
lang : metadata.lang
})); }));
}; localStorage.setItem(BREWKEY, text);
},
const save = async ()=>{ handleStyleChange : function(style){
setIsSaving(true); this.setState((prevState)=>({
brew : { ...prevState.brew, style: style },
}));
localStorage.setItem(STYLEKEY, style);
},
const 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();});
},
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);
}
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
const res = await request const res = await request
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) .post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
.send(updatedBrew) .send(brew)
.catch((err)=>{ .catch((err)=>{
setIsSaving(false); this.setState({ isSaving: false, error: err });
setError(err);
}); });
setIsSaving(false);
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 <Navbar>
setIsSaving(false);
};
const renderNavbar = ()=>(
<Navbar>
<Nav.section> <Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item> <Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section> </Nav.section>
<Nav.section> <Nav.section>
{error {this.state.error ?
? <ErrorNavItem error={error} clearError={clearError} /> <ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
: renderSaveButton()} this.renderSaveButton()
}
<PrintNavItem /> <PrintNavItem />
<HelpNavItem /> <HelpNavItem />
<RecentNavItem /> <RecentNavItem />
<AccountNavItem /> <AccountNavItem />
</Nav.section> </Nav.section>
</Navbar> </Navbar>;
); },
return ( render : function(){
<div className='newPage sitePage'> return <div className='newPage sitePage'>
{renderNavbar()} {this.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}
onTextChange={handleTextChange} onTextChange={this.handleTextChange}
onStyleChange={handleStyleChange} onStyleChange={this.handleStyleChange}
onMetaChange={handleMetaChange} onMetaChange={this.handleMetaChange}
onSnipChange={handleSnipChange} onSnipChange={this.handleSnipChange}
renderer={currentBrew.renderer} renderer={this.state.brew.renderer}
userThemes={props.userThemes} userThemes={this.props.userThemes}
themeBundle={themeBundle} themeBundle={this.state.themeBundle}
onCursorPageChange={handleEditorCursorPageChange} onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange} 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}
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={handleBrewRendererPageChange} 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> </div>;
); }
}; });
module.exports = NewPage; module.exports = NewPage;

View File

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

View File

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

View File

@@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx'); const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx');
const BrewItem = require('../basePages/listPage/brewItem/brewItem.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'); const ErrorIndex = require('../errorPage/errors/errorIndex.js');
import request from '../../utils/request-middleware.js'; import request from '../../utils/request-middleware.js';

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');
});
});

1510
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -93,7 +93,7 @@
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.6", "codemirror": "^6.0.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"core-js": "^3.44.0", "core-js": "^3.44.0",
"cors": "^2.8.5", "cors": "^2.8.5",
@@ -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.34.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.0.5", "jest": "^30.0.4",
"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.23.1", "stylelint": "^16.21.1",
"stylelint-config-recess-order": "^7.2.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.3"
} }
} }

View File

@@ -383,7 +383,6 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
title : req.brew.title || 'Untitled Brew', title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.', description : req.brew.description || 'No description.',
image : req.brew.thumbnail || defaultMetaTags.image, image : req.brew.thumbnail || defaultMetaTags.image,
locale : req.brew.lang,
type : 'article' type : 'article'
}; };
@@ -487,8 +486,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

@@ -52,13 +52,13 @@ const api = {
// ID Validation Checks // ID Validation Checks
// Homebrewery ID // Homebrewery ID
// Typically 12 characters, but the DB shows a range of 7 to 14 characters // Typically 12 characters, but the DB shows a range of 7 to 14 characters
if(!id.match(/^[a-zA-Z0-9-_]{7,14}$/)){ if(!id.match(/^[A-Za-z0-9_-]{7,14}$/)){
throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id }; throw { name: 'ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '11', brewId: id };
} }
// Google ID // Google ID
// Typically 33 characters, old format is 44 - always starts with a 1 // Typically 33 characters, old format is 44 - always starts with a 1
// Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable // Managed by Google, may change outside of our control, so any length between 33 and 44 is acceptable
if(googleId && !googleId.match(/^1(?:[a-zA-Z0-9-_]{32,43})$/)){ if(googleId && !googleId.match(/^1(?:[A-Za-z0-9+\/]{32,43})$/)){
throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id }; throw { name: 'Google ID Error', message: 'Invalid ID', status: 404, HBErrorCode: '12', brewId: id };
} }
@@ -375,14 +375,14 @@ const api = {
try { try {
const patches = parsePatch(brewFromClient.patches); const patches = parsePatch(brewFromClient.patches);
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error. // Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]); const patchedResult = applyPatches(patches, brewFromServer.text, { allowExceedingIndices: true })[0];
if(patchedResult != brewFromClient.text) if(patchedResult != brewFromClient.text)
throw("Patches did not apply cleanly, text mismatch detected"); throw("Patches did not apply cleanly, text mismatch detected");
// brew.text = applyPatches(patches, brewFromServer.text)[0]; // brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) { } catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`); //debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
console.error('Failed to apply patches:', { console.error('Failed to apply patches:', {
//patches : brewFromClient.patches, patches : brewFromClient.patches,
brewId : brewFromClient.editId || 'unknown', brewId : brewFromClient.editId || 'unknown',
error : err error : err
}); });

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

@@ -185,7 +185,7 @@ const mustacheSpans = {
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g;
const match = completeSpan.exec(src); const match = completeSpan.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -242,7 +242,7 @@ const mustacheDivs = {
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm;
const match = completeBlock.exec(src); const match = completeBlock.exec(src);
if(match) { if(match) {
//Find closing delimiter //Find closing delimiter
@@ -297,7 +297,7 @@ const mustacheInjectInline = {
level : 'inline', level : 'inline',
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];
@@ -343,7 +343,7 @@ const mustacheInjectBlock = {
level : 'block', level : 'block',
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
tokenizer(src, tokens) { tokenizer(src, tokens) {
const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym;
const match = inlineRegex.exec(src); const match = inlineRegex.exec(src);
if(match) { if(match) {
const lastToken = tokens[tokens.length - 1]; const lastToken = tokens[tokens.length - 1];