mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-26 09:33:02 +00:00
Compare commits
52 Commits
RemoveHand
...
LintEveryt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63bebe1efd | ||
|
|
22e26d635a | ||
|
|
643e0ac650 | ||
|
|
5395412ac5 | ||
|
|
dc4610ea1b | ||
|
|
1e71e9e18a | ||
|
|
4203e90d09 | ||
|
|
dc94555c94 | ||
|
|
41aebf084b | ||
|
|
74e17e154f | ||
|
|
a944b23ca0 | ||
|
|
12052853db | ||
|
|
c0f67bef5a | ||
|
|
8f715a6615 | ||
|
|
1f51abaf10 | ||
|
|
c90a8c53a5 | ||
|
|
ac18f4bd1d | ||
|
|
7393aef806 | ||
|
|
2c4c4b8f92 | ||
|
|
c751d647d9 | ||
|
|
6057b35d19 | ||
|
|
521d42f32f | ||
|
|
e9f8302597 | ||
|
|
f429b1755d | ||
|
|
ae51213c8c | ||
|
|
8f7ae35f08 | ||
|
|
f0bb06e706 | ||
|
|
aff9a85769 | ||
|
|
e0379a0baa | ||
|
|
e8a0681015 | ||
|
|
3ed61ebe2c | ||
|
|
c2e51b0baa | ||
|
|
bc258f5239 | ||
|
|
ba23763294 | ||
|
|
51b91567f6 | ||
|
|
eefda9fe45 | ||
|
|
e793db7b37 | ||
|
|
ff5450ad8c | ||
|
|
07495b0dea | ||
|
|
c6ed67db08 | ||
|
|
c50c279ef3 | ||
|
|
cc246fb31a | ||
|
|
788324fe31 | ||
|
|
da8772daa7 | ||
|
|
4c5eef46a0 | ||
|
|
a1ab27b57f | ||
|
|
d6a5a1f03c | ||
|
|
f04d6cdd1f | ||
|
|
4fd61ce92c | ||
|
|
88b70d340e | ||
|
|
ed05d8c754 | ||
|
|
077aaeb815 |
@@ -295,12 +295,6 @@ const BrewRenderer = (props)=>{
|
|||||||
rowGap : `${displayOptions.rowGap}px`
|
rowGap : `${displayOptions.rowGap}px`
|
||||||
};
|
};
|
||||||
|
|
||||||
const styleObject = {};
|
|
||||||
|
|
||||||
if(global.config.deployment) {
|
|
||||||
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
||||||
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
|
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
|
||||||
|
|
||||||
@@ -329,10 +323,9 @@ const BrewRenderer = (props)=>{
|
|||||||
contentDidMount={frameDidMount}
|
contentDidMount={frameDidMount}
|
||||||
onClick={()=>{emitClick();}}
|
onClick={()=>{emitClick();}}
|
||||||
>
|
>
|
||||||
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
<div className='brewRenderer'
|
||||||
onKeyDown={handleControlKeys}
|
onKeyDown={handleControlKeys}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
style={ styleObject }
|
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
&:has(.facing, .flow) { padding : 60px 30px; }
|
&:has(.facing, .flow) { padding : 60px 30px; }
|
||||||
&.deployment { background-color : darkred; }
|
|
||||||
:where(.pages) {
|
:where(.pages) {
|
||||||
&.facing {
|
&.facing {
|
||||||
display : grid;
|
display : grid;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module.exports = {
|
|||||||
try {
|
try {
|
||||||
Boolean(new URL(value));
|
Boolean(new URL(value));
|
||||||
return null;
|
return null;
|
||||||
} catch (e) {
|
} catch {
|
||||||
return 'Must be a valid URL';
|
return 'Must be a valid URL';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const WithRoute = ({ el: Element, ...rest })=>{
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const queryParams = Object.fromEntries(searchParams?.entries() || []);
|
const queryParams = Object.fromEntries(searchParams?.entries() || []);
|
||||||
|
|
||||||
return <Element {...rest} {...params} query={queryParams} />;
|
return <Element {...rest} {...params} query={queryParams} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,11 +49,20 @@ const Homebrew = (props)=>{
|
|||||||
global.enable_themes = enable_themes;
|
global.enable_themes = enable_themes;
|
||||||
global.config = config;
|
global.config = config;
|
||||||
|
|
||||||
|
const backgroundObject = ()=>{
|
||||||
|
if(global.config.deployment || (config.local && config.development)){
|
||||||
|
const bgText = global.config.deployment || 'Local';
|
||||||
|
return {
|
||||||
|
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
updateLocalStorage();
|
updateLocalStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router location={url}>
|
<Router location={url}>
|
||||||
<div className='homebrew'>
|
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
|
||||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
|
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
@import 'naturalcrit/styles/core.less';
|
@import 'naturalcrit/styles/core.less';
|
||||||
.homebrew {
|
.homebrew {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
|
background-color:@steel;
|
||||||
|
&.deployment { background-color : darkred; }
|
||||||
|
|
||||||
.sitePage {
|
.sitePage {
|
||||||
display : flex;
|
display : flex;
|
||||||
flex-direction : column;
|
flex-direction : column;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
overflow-y : hidden;
|
overflow-y : hidden;
|
||||||
background-color : @steel;
|
|
||||||
.content {
|
.content {
|
||||||
position : relative;
|
position : relative;
|
||||||
flex : auto;
|
flex : auto;
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ 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 ErrorNavItem = ({error = '', clearError})=>{
|
const ErrorNavItem = ({ error = '', clearError })=>{
|
||||||
const response = error.response;
|
const response = error.response;
|
||||||
const errorCode = error.code
|
const errorCode = error.code;
|
||||||
const status = response?.status;
|
const status = response?.status;
|
||||||
const HBErrorCode = response?.body?.HBErrorCode;
|
const HBErrorCode = response?.body?.HBErrorCode;
|
||||||
const message = response?.body?.message;
|
const message = response?.body?.message;
|
||||||
@@ -15,7 +15,7 @@ const ErrorNavItem = ({error = '', clearError})=>{
|
|||||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||||
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
||||||
console.log(errMsg);
|
console.log(errMsg);
|
||||||
} catch (e){}
|
} catch {}
|
||||||
|
|
||||||
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'>
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ const NewBrew = ()=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const type = file.name.split('.').pop().toLowerCase();
|
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.`);
|
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);
|
console.log(file);
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ import React from 'react';
|
|||||||
import dedent from 'dedent-tabs';
|
import dedent from 'dedent-tabs';
|
||||||
import Nav from 'naturalcrit/nav/nav.jsx';
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
|
|
||||||
const getShareId = (brew)=>(
|
const getShareId = (brew)=>(
|
||||||
brew.googleId && !brew.stubbed
|
brew.googleId && !brew.stubbed
|
||||||
? brew.googleId + brew.shareId
|
? brew.googleId + brew.shareId
|
||||||
: brew.shareId
|
: brew.shareId
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRedditLink = (brew)=>{
|
const getRedditLink = (brew)=>{
|
||||||
const text = dedent`
|
const text = dedent`
|
||||||
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||||
|
|
||||||
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
|
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({brew}) => (
|
export default ({ brew })=>(
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||||
share
|
share
|
||||||
|
|||||||
@@ -1,38 +1,38 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import './editPage.less';
|
import './editPage.less';
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
// Common imports
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import _ from 'lodash';;
|
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||||
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
|
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||||
import { md5 } from 'hash-wasm';
|
|
||||||
import { gzipSync, strToU8 } from 'fflate';
|
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||||
import { Meta } from 'vitreum/headtags';
|
import Editor from '../../editor/editor.jsx';
|
||||||
|
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||||
|
|
||||||
import Nav from 'naturalcrit/nav/nav.jsx';
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
import Navbar from '../../navbar/navbar.jsx';
|
import Navbar from '../../navbar/navbar.jsx';
|
||||||
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
||||||
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||||
import ShareNavItem from '../../navbar/share.navitem.jsx';
|
|
||||||
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||||
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||||
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
||||||
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
||||||
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||||
|
|
||||||
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
// Page specific imports
|
||||||
import Editor from '../../editor/editor.jsx';
|
import { Meta } from 'vitreum/headtags';
|
||||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
import { md5 } from 'hash-wasm';
|
||||||
|
import { gzipSync, strToU8 } from 'fflate';
|
||||||
|
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
|
||||||
|
|
||||||
|
import ShareNavItem from '../../navbar/share.navitem.jsx';
|
||||||
import LockNotification from './lockNotification/lockNotification.jsx';
|
import LockNotification from './lockNotification/lockNotification.jsx';
|
||||||
|
|
||||||
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
|
||||||
import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js';
|
|
||||||
|
|
||||||
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||||
|
|
||||||
import googleDriveIcon from '../../googleDrive.svg';
|
import googleDriveIcon from '../../googleDrive.svg';
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 10000;
|
const SAVE_TIMEOUT = 10000;
|
||||||
@@ -46,6 +46,8 @@ const STYLEKEY = 'HB_newPage_style';
|
|||||||
const SNIPKEY = 'HB_newPage_snippets';
|
const SNIPKEY = 'HB_newPage_snippets';
|
||||||
const METAKEY = 'HB_newPage_meta';
|
const METAKEY = 'HB_newPage_meta';
|
||||||
|
|
||||||
|
const useLocalStorage = false;
|
||||||
|
const neverSaved = false;
|
||||||
|
|
||||||
const EditPage = (props)=>{
|
const EditPage = (props)=>{
|
||||||
props = {
|
props = {
|
||||||
@@ -77,8 +79,6 @@ const EditPage = (props)=>{
|
|||||||
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
|
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
|
||||||
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
|
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
|
||||||
|
|
||||||
const useLocalStorage = false;
|
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
|
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
|
||||||
setAutoSaveEnabled(autoSavePref);
|
setAutoSaveEnabled(autoSavePref);
|
||||||
@@ -123,16 +123,16 @@ const EditPage = (props)=>{
|
|||||||
editorRef.current?.update();
|
editorRef.current?.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||||
if (subfield == 'renderer' || subfield == 'theme')
|
if(subfield == 'renderer' || subfield == 'theme')
|
||||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||||
|
|
||||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||||
setHTMLErrors(Markdown.validate(value));
|
setHTMLErrors(Markdown.validate(value));
|
||||||
|
|
||||||
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
|
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||||
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
|
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||||
|
|
||||||
if(useLocalStorage) {
|
if(useLocalStorage) {
|
||||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||||
@@ -309,14 +309,18 @@ const EditPage = (props)=>{
|
|||||||
|
|
||||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||||
if(unsavedChanges)
|
if(unsavedChanges)
|
||||||
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
||||||
|
|
||||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||||
if(autoSaveEnabled)
|
if(autoSaveEnabled)
|
||||||
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
return <Nav.item className='save saved'>auto-saved</Nav.item>;
|
||||||
|
|
||||||
|
// #5 - No unsaved changes, and has never been saved, hide the button
|
||||||
|
if(neverSaved)
|
||||||
|
return <Nav.item className='save neverSaved'>save now</Nav.item>;
|
||||||
|
|
||||||
// DEFAULT - No unsaved changes, show SAVED
|
// DEFAULT - No unsaved changes, show SAVED
|
||||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAutoSave = ()=>{
|
const toggleAutoSave = ()=>{
|
||||||
@@ -352,11 +356,11 @@ const EditPage = (props)=>{
|
|||||||
{renderSaveButton()}
|
{renderSaveButton()}
|
||||||
{renderAutoSaveButton()}
|
{renderAutoSaveButton()}
|
||||||
</Nav.dropdown>}
|
</Nav.dropdown>}
|
||||||
<NewBrewItem/>
|
<NewBrewItem />
|
||||||
<HelpNavItem/>
|
|
||||||
<ShareNavItem brew={currentBrew} />
|
|
||||||
<PrintNavItem />
|
<PrintNavItem />
|
||||||
|
<HelpNavItem />
|
||||||
<VaultNavItem />
|
<VaultNavItem />
|
||||||
|
<ShareNavItem brew={currentBrew} />
|
||||||
<RecentNavItem brew={currentBrew} storageKey='edit' />
|
<RecentNavItem brew={currentBrew} storageKey='edit' />
|
||||||
<AccountNavItem/>
|
<AccountNavItem/>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
@@ -1,55 +1,79 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
import './homePage.less';
|
import './homePage.less';
|
||||||
|
|
||||||
import React from 'react';
|
// Common imports
|
||||||
import { useEffect, useState, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
import { Meta } from 'vitreum/headtags';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import Nav from 'naturalcrit/nav/nav.jsx';
|
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||||
import Navbar from '../../navbar/navbar.jsx';
|
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||||
import NewBrewItem from '../../navbar/newbrew.navitem.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';
|
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||||
import Editor from '../../editor/editor.jsx';
|
import Editor from '../../editor/editor.jsx';
|
||||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||||
|
|
||||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
|
import Navbar from '../../navbar/navbar.jsx';
|
||||||
|
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
||||||
|
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||||
|
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||||
|
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||||
|
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
||||||
|
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
||||||
|
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||||
|
|
||||||
|
// Page specific imports
|
||||||
|
import { Meta } from 'vitreum/headtags';
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
const SNIPKEY = 'homebrewery-new-snippets';
|
const SNIPKEY = 'homebrewery-new-snippets';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
|
||||||
|
const useLocalStorage = false;
|
||||||
|
const neverSaved = true;
|
||||||
|
|
||||||
const HomePage =(props)=>{
|
const HomePage =(props)=>{
|
||||||
props = {
|
props = {
|
||||||
brew : DEFAULT_BREW,
|
brew : DEFAULT_BREW,
|
||||||
ver : '0.0.0',
|
ver : '0.0.0',
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
|
||||||
const [currentBrew , setCurrentBrew] = useState(props.brew);
|
const [currentBrew , setCurrentBrew] = useState(props.brew);
|
||||||
const [welcomeText , setWelcomeText] = useState(props.brew.text);
|
|
||||||
const [error , setError] = useState(undefined);
|
const [error , setError] = useState(undefined);
|
||||||
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
|
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text));
|
||||||
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
|
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
|
||||||
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||||
const [themeBundle , setThemeBundle] = useState({});
|
const [themeBundle , setThemeBundle] = useState({});
|
||||||
|
const [unsavedChanges , setUnsavedChanges] = useState(false);
|
||||||
const [isSaving , setIsSaving] = useState(false);
|
const [isSaving , setIsSaving] = useState(false);
|
||||||
|
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
|
||||||
|
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
|
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||||
const useLocalStorage = false;
|
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||||
|
|
||||||
|
const handleControlKeys = (e)=>{
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
if(e.keyCode === 83) trySaveRef.current(true);
|
||||||
|
if(e.keyCode === 80) printCurrentBrew();
|
||||||
|
if([83, 80].includes(e.keyCode)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
|
|
||||||
|
return ()=>{
|
||||||
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const save = ()=>{
|
const save = ()=>{
|
||||||
@@ -65,20 +89,27 @@ const HomePage =(props)=>{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||||
|
setUnsavedChanges(hasChange);
|
||||||
|
|
||||||
|
if(autoSaveEnabled) trySave(false, hasChange);
|
||||||
|
}, [currentBrew]);
|
||||||
|
|
||||||
const handleSplitMove = ()=>{
|
const handleSplitMove = ()=>{
|
||||||
editorRef.current.update();
|
editorRef.current.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||||
if (subfield == 'renderer' || subfield == 'theme')
|
if(subfield == 'renderer' || subfield == 'theme')
|
||||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||||
|
|
||||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||||
setHTMLErrors(Markdown.validate(value));
|
setHTMLErrors(Markdown.validate(value));
|
||||||
|
|
||||||
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
|
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||||
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
|
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||||
|
|
||||||
if(useLocalStorage) {
|
if(useLocalStorage) {
|
||||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||||
@@ -92,6 +123,41 @@ const HomePage =(props)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// resetWarnUnsavedTimer();
|
||||||
|
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
|
||||||
|
// const text = elapsedTime === 0
|
||||||
|
// ? 'Autosave is OFF.'
|
||||||
|
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||||
|
|
||||||
|
// return <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} 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>;
|
||||||
|
|
||||||
|
// #5 - No unsaved changes, and has never been saved, hide the button
|
||||||
|
if(neverSaved)
|
||||||
|
return <Nav.item className='save neverSaved'>save now</Nav.item>;
|
||||||
|
|
||||||
|
// DEFAULT - No unsaved changes, show SAVED
|
||||||
|
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||||
|
};
|
||||||
|
|
||||||
const clearError = ()=>{
|
const clearError = ()=>{
|
||||||
setError(null);
|
setError(null);
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
@@ -100,11 +166,11 @@ const HomePage =(props)=>{
|
|||||||
const renderNavbar = ()=>{
|
const renderNavbar = ()=>{
|
||||||
return <Navbar ver={props.ver}>
|
return <Navbar ver={props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{error ?
|
{error
|
||||||
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
|
? <ErrorNavItem error={error} clearError={clearError} />
|
||||||
null
|
: renderSaveButton()}
|
||||||
}
|
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
|
<PrintNavItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<VaultNavItem />
|
<VaultNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
@@ -144,7 +210,7 @@ const HomePage =(props)=>{
|
|||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
<div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}>
|
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
|
||||||
Save current <i className='fas fa-save' />
|
Save current <i className='fas fa-save' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -152,7 +218,7 @@ const HomePage =(props)=>{
|
|||||||
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;
|
||||||
|
|||||||
@@ -34,7 +34,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navItem.save {
|
.navItem.save {
|
||||||
|
.fadeInRight();
|
||||||
|
.transition(opacity);
|
||||||
background-color : @orange;
|
background-color : @orange;
|
||||||
&:hover { background-color : @green; }
|
&:hover { background-color : @green; }
|
||||||
|
&.neverSaved {
|
||||||
|
.fadeOutRight();
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/* eslint-disable max-lines */
|
||||||
import './newPage.less';
|
import './newPage.less';
|
||||||
|
|
||||||
|
// Common imports
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import Nav from 'naturalcrit/nav/nav.jsx';
|
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||||
import Navbar from '../../navbar/navbar.jsx';
|
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||||
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
|
||||||
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
|
||||||
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
|
||||||
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
|
||||||
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
|
||||||
|
|
||||||
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||||
import Editor from '../../editor/editor.jsx';
|
import Editor from '../../editor/editor.jsx';
|
||||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||||
|
|
||||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
import Navbar from '../../navbar/navbar.jsx';
|
||||||
|
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
||||||
|
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||||
|
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||||
|
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||||
|
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
||||||
|
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
||||||
|
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||||
|
|
||||||
|
// Page specific imports
|
||||||
|
import { Meta } from 'vitreum/headtags';
|
||||||
|
|
||||||
const BREWKEY = 'HB_newPage_content';
|
const BREWKEY = 'HB_newPage_content';
|
||||||
const STYLEKEY = 'HB_newPage_style';
|
const STYLEKEY = 'HB_newPage_style';
|
||||||
@@ -27,10 +33,12 @@ const METAKEY = 'HB_newPage_metadata';
|
|||||||
const SNIPKEY = 'HB_newPage_snippets';
|
const SNIPKEY = 'HB_newPage_snippets';
|
||||||
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
|
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
|
||||||
|
|
||||||
|
const useLocalStorage = true;
|
||||||
|
const neverSaved = true;
|
||||||
|
|
||||||
const NewPage = (props) => {
|
const NewPage = (props)=>{
|
||||||
props = {
|
props = {
|
||||||
brew: DEFAULT_BREW,
|
brew : DEFAULT_BREW,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,17 +51,29 @@ const NewPage = (props) => {
|
|||||||
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||||
const [themeBundle , setThemeBundle ] = useState({});
|
const [themeBundle , setThemeBundle ] = useState({});
|
||||||
|
const [unsavedChanges , setUnsavedChanges ] = useState(false);
|
||||||
|
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
|
||||||
|
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
|
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||||
|
|
||||||
const useLocalStorage = true;
|
useEffect(()=>{
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
document.addEventListener('keydown', handleControlKeys);
|
|
||||||
loadBrew();
|
loadBrew();
|
||||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||||
|
|
||||||
return () => {
|
const handleControlKeys = (e)=>{
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
if(e.keyCode === 83) trySaveRef.current(true);
|
||||||
|
if(e.keyCode === 80) printCurrentBrew();
|
||||||
|
if([83, 80].includes(e.keyCode)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
|
|
||||||
|
return ()=>{
|
||||||
document.removeEventListener('keydown', handleControlKeys);
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -76,6 +96,7 @@ const NewPage = (props) => {
|
|||||||
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||||
|
|
||||||
setCurrentBrew(brew);
|
setCurrentBrew(brew);
|
||||||
|
lastSavedBrew.current = brew;
|
||||||
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
|
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
@@ -86,32 +107,27 @@ const NewPage = (props) => {
|
|||||||
window.history.replaceState({}, window.location.title, '/new/');
|
window.history.replaceState({}, window.location.title, '/new/');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleControlKeys = (e) => {
|
useEffect(()=>{
|
||||||
if (!(e.ctrlKey || e.metaKey)) return;
|
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||||
const S_KEY = 83;
|
setUnsavedChanges(hasChange);
|
||||||
const P_KEY = 80;
|
|
||||||
if (e.keyCode === S_KEY) save();
|
if(autoSaveEnabled) trySave(false, hasChange);
|
||||||
if (e.keyCode === P_KEY) printCurrentBrew();
|
}, [currentBrew]);
|
||||||
if (e.keyCode === S_KEY || e.keyCode === P_KEY) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSplitMove = ()=>{
|
const handleSplitMove = ()=>{
|
||||||
editorRef.current.update();
|
editorRef.current.update();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||||
if (subfield == 'renderer' || subfield == 'theme')
|
if(subfield == 'renderer' || subfield == 'theme')
|
||||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||||
|
|
||||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||||
setHTMLErrors(Markdown.validate(value));
|
setHTMLErrors(Markdown.validate(value));
|
||||||
|
|
||||||
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
|
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||||
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
|
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||||
|
|
||||||
if(useLocalStorage) {
|
if(useLocalStorage) {
|
||||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||||
@@ -125,10 +141,10 @@ const NewPage = (props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async () => {
|
const save = async ()=>{
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
let updatedBrew = { ...currentBrew };
|
const updatedBrew = { ...currentBrew };
|
||||||
splitTextStyleAndMetadata(updatedBrew);
|
splitTextStyleAndMetadata(updatedBrew);
|
||||||
|
|
||||||
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
|
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
|
||||||
@@ -137,13 +153,13 @@ const NewPage = (props) => {
|
|||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||||
.send(updatedBrew)
|
.send(updatedBrew)
|
||||||
.catch((err) => {
|
.catch((err)=>{
|
||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
setError(err);
|
setError(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsSaving(false)
|
setIsSaving(false);
|
||||||
if (!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
const savedBrew = res.body;
|
const savedBrew = res.body;
|
||||||
|
|
||||||
@@ -154,15 +170,38 @@ const NewPage = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderSaveButton = ()=>{
|
const renderSaveButton = ()=>{
|
||||||
if(isSaving){
|
// #1 - Currently saving, show SAVING
|
||||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
if(isSaving)
|
||||||
save...
|
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||||
</Nav.item>;
|
|
||||||
} else {
|
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||||
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
|
// if(unsavedChanges && warnUnsavedChanges) {
|
||||||
save
|
// resetWarnUnsavedTimer();
|
||||||
</Nav.item>;
|
// 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} 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>;
|
||||||
|
|
||||||
|
// #5 - No unsaved changes, and has never been saved, hide the button
|
||||||
|
if(neverSaved)
|
||||||
|
return <Nav.item className='save neverSaved'>save now</Nav.item>;
|
||||||
|
|
||||||
|
// DEFAULT - No unsaved changes, show SAVED
|
||||||
|
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearError = ()=>{
|
const clearError = ()=>{
|
||||||
@@ -170,7 +209,7 @@ const NewPage = (props) => {
|
|||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderNavbar = () => (
|
const renderNavbar = ()=>(
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||||
@@ -180,8 +219,10 @@ const NewPage = (props) => {
|
|||||||
{error
|
{error
|
||||||
? <ErrorNavItem error={error} clearError={clearError} />
|
? <ErrorNavItem error={error} clearError={clearError} />
|
||||||
: renderSaveButton()}
|
: renderSaveButton()}
|
||||||
|
<NewBrewItem />
|
||||||
<PrintNavItem />
|
<PrintNavItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
|
<VaultNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
@@ -189,7 +230,7 @@ const NewPage = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='newPage sitePage'>
|
<div className='newPage sitePage'>
|
||||||
{renderNavbar()}
|
{renderNavbar()}
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={handleSplitMove}>
|
<SplitPane onDragFinish={handleSplitMove}>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
.newPage {
|
.newPage {
|
||||||
.navItem.save {
|
.navItem.save {
|
||||||
|
.fadeInRight();
|
||||||
|
.transition(opacity);
|
||||||
background-color : @orange;
|
background-color : @orange;
|
||||||
&:hover { background-color : @green; }
|
&:hover { background-color : @green; }
|
||||||
|
&.neverSaved {
|
||||||
|
.fadeOutRight();
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
.vaultPage {
|
.vaultPage {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
overflow-y : hidden;
|
overflow-y : hidden;
|
||||||
background-color : #2C3E50;
|
|
||||||
|
|
||||||
*:not(input) { user-select : none; }
|
*:not(input) { user-select : none; }
|
||||||
|
|
||||||
|
.form {
|
||||||
|
background:white;
|
||||||
|
}
|
||||||
|
|
||||||
:where(.content .dataGroup) {
|
:where(.content .dataGroup) {
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
background : white;
|
|
||||||
|
|
||||||
&.form .brewLookup {
|
&.form .brewLookup {
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -171,7 +173,6 @@
|
|||||||
max-height : 100%;
|
max-height : 100%;
|
||||||
padding : 70px 50px;
|
padding : 70px 50px;
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
background-color : #2C3E50;
|
|
||||||
container-type : inline-size;
|
container-type : inline-size;
|
||||||
|
|
||||||
h3 { font-size : 25px; }
|
h3 { font-size : 25px; }
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const getLocalStorageMap = function(){
|
|||||||
|
|
||||||
if(global?.account?.username){
|
if(global?.account?.username){
|
||||||
const username = global.account.username;
|
const username = global.account.username;
|
||||||
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
|
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return localStorageMap;
|
return localStorageMap;
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import getLocalStorageMap from './localStorageKeyMap.js';
|
|
||||||
|
|
||||||
describe('getLocalStorageMap', ()=>{
|
|
||||||
it('no username', ()=>{
|
|
||||||
const account = global.account;
|
|
||||||
|
|
||||||
delete global.account;
|
|
||||||
|
|
||||||
const map = getLocalStorageMap();
|
|
||||||
|
|
||||||
global.account = account;
|
|
||||||
|
|
||||||
expect(map).toBeInstanceOf(Object);
|
|
||||||
expect(Object.entries(map)).toHaveLength(16);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('no username', ()=>{
|
|
||||||
const account = global.account;
|
|
||||||
|
|
||||||
global.account = { username: 'test' };
|
|
||||||
|
|
||||||
const map = getLocalStorageMap();
|
|
||||||
|
|
||||||
global.account = account;
|
|
||||||
|
|
||||||
expect(map).toBeInstanceOf(Object);
|
|
||||||
expect(Object.entries(map)).toHaveLength(17);
|
|
||||||
expect(map).toHaveProperty('HOMEBREWERY-DEFAULT-SAVE-LOCATION-test', 'HB_editor_defaultSave_test');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,21 +4,16 @@ const updateLocalStorage = function(){
|
|||||||
// Return if no window and thus no local storage
|
// Return if no window and thus no local storage
|
||||||
if(typeof window === 'undefined') return;
|
if(typeof window === 'undefined') return;
|
||||||
|
|
||||||
// Return if the local storage key map has no content
|
|
||||||
const localStorageKeyMap = getLocalStorageMap();
|
const localStorageKeyMap = getLocalStorageMap();
|
||||||
if(Object.keys(localStorageKeyMap).length == 0) return;
|
|
||||||
|
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
|
|
||||||
const deleteKeys = storage?.getItem('HB_deleteKeys') != null;
|
|
||||||
|
|
||||||
Object.keys(localStorageKeyMap).forEach((key)=>{
|
Object.keys(localStorageKeyMap).forEach((key)=>{
|
||||||
if(storage[key]){
|
if(storage[key]){
|
||||||
if(!storage[localStorageKeyMap[key]]){
|
if(!storage[localStorageKeyMap[key]]){
|
||||||
const data = storage.getItem(key);
|
const data = storage.getItem(key);
|
||||||
storage.setItem(localStorageKeyMap[key], data);
|
storage.setItem(localStorageKeyMap[key], data);
|
||||||
};
|
};
|
||||||
if(deleteKeys) storage.removeItem(key);
|
storage.removeItem(key);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"development": true,
|
||||||
"host" : "homebrewery.local.naturalcrit.com:8000",
|
"host" : "homebrewery.local.naturalcrit.com:8000",
|
||||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||||
"secret" : "secret",
|
"secret" : "secret",
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import Markdown from '../shared/naturalcrit/markdown.js';
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import asyncHandler from 'express-async-handler';
|
import asyncHandler from 'express-async-handler';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
|
import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch';
|
||||||
import { md5 } from 'hash-wasm';
|
import { md5 } from 'hash-wasm';
|
||||||
import { splitTextStyleAndMetadata,
|
import { splitTextStyleAndMetadata,
|
||||||
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
|
||||||
import checkClientVersion from './middleware/check-client-version.js';
|
import checkClientVersion from './middleware/check-client-version.js';
|
||||||
|
|
||||||
@@ -377,14 +377,14 @@ const api = {
|
|||||||
// 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 = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[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
|
||||||
});
|
});
|
||||||
// While running in parallel, don't throw the error upstream.
|
// While running in parallel, don't throw the error upstream.
|
||||||
// throw err; // rethrow to preserve the 500 behavior
|
// throw err; // rethrow to preserve the 500 behavior
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
|
|||||||
const mpAsSnippets = [];
|
const mpAsSnippets = [];
|
||||||
// Snippets from Themes first.
|
// Snippets from Themes first.
|
||||||
if(themeBundleSnippets) {
|
if(themeBundleSnippets) {
|
||||||
for (let themes of themeBundleSnippets) {
|
for (const themes of themeBundleSnippets) {
|
||||||
if(typeof themes !== 'string') {
|
if(typeof themes !== 'string') {
|
||||||
const userSnippets = [];
|
const userSnippets = [];
|
||||||
const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
|
const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
|
||||||
@@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{
|
|||||||
if(typeof yamlObj == 'string') return yamlObj;
|
if(typeof yamlObj == 'string') return yamlObj;
|
||||||
|
|
||||||
let snippetsText = '';
|
let snippetsText = '';
|
||||||
|
|
||||||
for (let snippet of yamlObj) {
|
for (const snippet of yamlObj) {
|
||||||
for (let subSnippet of snippet.subsnippets) {
|
for (const subSnippet of snippet.subsnippets) {
|
||||||
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
|
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
|
|||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/theme/${renderer}/${theme}`)
|
.get(`/api/theme/${renderer}/${theme}`)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
setError(err)
|
setError(err);
|
||||||
});
|
});
|
||||||
if(!res) {
|
if(!res) {
|
||||||
setThemeBundle({});
|
setThemeBundle({});
|
||||||
@@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
|
|||||||
setError(null);
|
setError(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
|
||||||
const clientText = clientTextRaw?.normalize('NFC') || '';
|
const clientText = clientTextRaw?.normalize('NFC') || '';
|
||||||
const serverText = serverTextRaw?.normalize('NFC') || '';
|
const serverText = serverTextRaw?.normalize('NFC') || '';
|
||||||
|
|
||||||
const clientBuffer = Buffer.from(clientText, 'utf8');
|
const clientBuffer = Buffer.from(clientText, 'utf8');
|
||||||
const serverBuffer = Buffer.from(serverText, 'utf8');
|
const serverBuffer = Buffer.from(serverText, 'utf8');
|
||||||
|
|
||||||
if (clientBuffer.equals(serverBuffer)) {
|
if(clientBuffer.equals(serverBuffer)) {
|
||||||
console.log(`✅ ${label} text matches byte-for-byte.`);
|
console.log(`✅ ${label} text matches byte-for-byte.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
|||||||
|
|
||||||
// Byte-level diff
|
// Byte-level diff
|
||||||
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
|
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
|
||||||
if (clientBuffer[i] !== serverBuffer[i]) {
|
if(clientBuffer[i] !== serverBuffer[i]) {
|
||||||
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
|
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
|
|||||||
|
|
||||||
// Char-level diff
|
// Char-level diff
|
||||||
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
|
||||||
if (clientText[i] !== serverText[i]) {
|
if(clientText[i] !== serverText[i]) {
|
||||||
console.log(`Char mismatch at index ${i}:`);
|
console.log(`Char mismatch at index ${i}:`);
|
||||||
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||||
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
splitTextStyleAndMetadata,
|
splitTextStyleAndMetadata,
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return mathParser.evaluate(replacedLabel);
|
return mathParser.evaluate(replacedLabel);
|
||||||
} catch (error) {
|
} catch {
|
||||||
return undefined; // Return undefined if invalid math result
|
return undefined; // Return undefined if invalid math result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -680,7 +680,7 @@ const tableTerminators = [
|
|||||||
|
|
||||||
Marked.use(MarkedVariables());
|
Marked.use(MarkedVariables());
|
||||||
Marked.use(MarkedDefinitionLists());
|
Marked.use(MarkedDefinitionLists());
|
||||||
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
|
||||||
Marked.use(mustacheInjectBlock);
|
Marked.use(mustacheInjectBlock);
|
||||||
Marked.use(MarkedAlignedParagraphs());
|
Marked.use(MarkedAlignedParagraphs());
|
||||||
Marked.use(MarkedSubSuperText());
|
Marked.use(MarkedSubSuperText());
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
|
|||||||
prot = decodeURIComponent(unescape(href))
|
prot = decodeURIComponent(unescape(href))
|
||||||
.replace(nonWordAndColonTest, '')
|
.replace(nonWordAndColonTest, '')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
} catch (e) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
|
||||||
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
href = encodeURI(href).replace(/%25/g, '%');
|
href = encodeURI(href).replace(/%25/g, '%');
|
||||||
} catch (e) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return href;
|
return href;
|
||||||
|
|||||||
@@ -611,3 +611,17 @@ h6,
|
|||||||
}
|
}
|
||||||
.toc.wide li { break-inside : auto; }
|
.toc.wide li { break-inside : auto; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**********************************
|
||||||
|
Firefox endruns
|
||||||
|
**********************************/
|
||||||
|
|
||||||
|
@supports (-moz-user-select: none) { // This section will only apply to Firefox; it's the only browser that supports `-mos-xyz...`
|
||||||
|
.page {
|
||||||
|
blockquote, table {
|
||||||
|
page-break-inside: auto;
|
||||||
|
break-inside: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user