mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-09-23 20:32:56 +00:00
Merge branch 'master' into experimentalEventStreamEmitter
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
const React = require('react');
|
||||
const moment = require('moment');
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import UIPage from '../basePages/uiPage/uiPage.jsx';
|
||||
import NaturalCritIcon from '@components/svg/naturalcrit-d20.svg.jsx';
|
||||
|
||||
let SAVEKEY = '';
|
||||
|
||||
@@ -13,7 +13,7 @@ const AccountPage = (props)=>{
|
||||
// initialize save location from local storage based on user id
|
||||
React.useEffect(()=>{
|
||||
if(!saveLocation && accountDetails.username) {
|
||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
|
||||
SAVEKEY = `HB_editor_defaultSave_${accountDetails.username}`;
|
||||
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
|
||||
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||
@@ -79,4 +79,4 @@ const AccountPage = (props)=>{
|
||||
</UIPage>);
|
||||
};
|
||||
|
||||
module.exports = AccountPage;
|
||||
export default AccountPage;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
require('./brewItem.less');
|
||||
const React = require('react');
|
||||
const { useCallback } = React;
|
||||
const moment = require('moment');
|
||||
import './brewItem.less';
|
||||
import React, { useCallback } from 'react';
|
||||
import moment from 'moment';
|
||||
import request from '../../../../utils/request-middleware.js';
|
||||
|
||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||
const homebreweryIcon = require('../../../../thumbnail.svg');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
import googleDriveIcon from '../../../../googleDrive.svg';
|
||||
import homebreweryIcon from '../../../../thumbnail.svg';
|
||||
import dedent from 'dedent';
|
||||
|
||||
const BrewItem = ({
|
||||
brew = {
|
||||
@@ -40,9 +39,9 @@ const BrewItem = ({
|
||||
if(!brew.editId) return null;
|
||||
|
||||
return (
|
||||
<a className='deleteLink' onClick={deleteBrew}>
|
||||
<i className='fas fa-trash-alt' title='Delete' />
|
||||
</a>
|
||||
<button aria-label={`Delete ${brew.title}`} className='deleteLink' onClick={deleteBrew}>
|
||||
<i className='fas fa-trash-alt' aria-hidden='true' title='Delete' />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -53,7 +52,7 @@ const BrewItem = ({
|
||||
if(brew.googleId && !brew.stubbed) editLink = brew.googleId + editLink;
|
||||
|
||||
return (
|
||||
<a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<a className='editLink' href={`/edit/${editLink}`} aria-label={`Edit ${brew.title}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fas fa-pencil-alt' title='Edit' />
|
||||
</a>
|
||||
);
|
||||
@@ -68,7 +67,7 @@ const BrewItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<a className='shareLink' href={`/share/${shareLink}`} aria-label={`Share ${brew.title}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fas fa-share-alt' title='Share' />
|
||||
</a>
|
||||
);
|
||||
@@ -83,7 +82,7 @@ const BrewItem = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<a className='downloadLink' href={`/download/${shareLink}`}>
|
||||
<a className='downloadLink' aria-label={`Download ${brew.title}`} href={`/download/${shareLink}`}>
|
||||
<i className='fas fa-download' title='Download' />
|
||||
</a>
|
||||
);
|
||||
@@ -95,7 +94,7 @@ const BrewItem = ({
|
||||
return (
|
||||
<span title={brew.webViewLink ? 'Your Google Drive Storage' : 'Another User\'s Google Drive Storage'}>
|
||||
<a href={brew.webViewLink} target='_blank'>
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='Google Drive Storage' />
|
||||
</a>
|
||||
</span>
|
||||
);
|
||||
@@ -103,7 +102,7 @@ const BrewItem = ({
|
||||
|
||||
return (
|
||||
<span title='Homebrewery Storage'>
|
||||
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||
<img className='homebreweryIcon' src={homebreweryIcon} alt='Homebrewery Storage' />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -143,25 +142,26 @@ const BrewItem = ({
|
||||
<span title="Username contained an email address; hidden to protect user's privacy">
|
||||
{author}
|
||||
</span>
|
||||
) : (<a href={`/user/${author}`}>{author}</a>)}
|
||||
) : (<a href={`/user/${encodeURIComponent(author)}`}>{author}</a>)}
|
||||
{index < brew.authors.length - 1 && ', '}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</span>
|
||||
<br />
|
||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||
<i className='fas fa-eye' /> {brew.views}
|
||||
<span aria-label={`Viewed ${brew.views} times`} title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||
<span aria-hidden='true'><i className='fas fa-eye' /> {brew.views}</span>
|
||||
</span>
|
||||
{brew.pageCount && (
|
||||
<span title={`Page count: ${brew.pageCount}`}>
|
||||
<i className='far fa-file' /> {brew.pageCount}
|
||||
<span aria-label={`${brew.pageCount} pages`} title={`Page count: ${brew.pageCount}`}>
|
||||
<span aria-hidden='true'><i className='far fa-file' /> {brew.pageCount}</span>
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
aria-label={`Last updated ${moment(brew.updatedAt).fromNow()}`}
|
||||
title={dedent` Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}
|
||||
>
|
||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||
<span aria-hidden='true'><i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}</span>
|
||||
</span>
|
||||
{renderStorageIcon()}
|
||||
</div>
|
||||
@@ -176,4 +176,4 @@ const BrewItem = ({
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = BrewItem;
|
||||
export default BrewItem;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '@sharedStyles/core.less';
|
||||
|
||||
.brewItem {
|
||||
position : relative;
|
||||
@@ -88,7 +89,7 @@
|
||||
&::before { content : '\f518'; }
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
&:hover, &:focus-within {
|
||||
.links { opacity : 1; }
|
||||
}
|
||||
&:nth-child(2n + 1) { margin-right : 0px; }
|
||||
@@ -102,7 +103,7 @@
|
||||
text-align : center;
|
||||
background-color : fade(black, 60%);
|
||||
opacity : 0;
|
||||
a {
|
||||
a, button {
|
||||
.animate(opacity);
|
||||
display : block;
|
||||
margin : 8px 0px;
|
||||
@@ -110,6 +111,7 @@
|
||||
color : white;
|
||||
text-decoration : unset;
|
||||
opacity : 0.6;
|
||||
width : 100%;
|
||||
&:hover { opacity : 1; }
|
||||
i { cursor : pointer; }
|
||||
}
|
||||
|
||||
@@ -1,143 +1,128 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./listPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
import './listPage.less';
|
||||
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||
import BrewItem from './brewItem/brewItem.jsx';
|
||||
|
||||
const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
|
||||
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir';
|
||||
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
|
||||
const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
|
||||
|
||||
const DEFAULT_SORT_TYPE = 'alpha';
|
||||
const DEFAULT_SORT_DIR = 'asc';
|
||||
|
||||
const ListPage = createClass({
|
||||
displayName : 'ListPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brewCollection : [
|
||||
{
|
||||
title : '',
|
||||
class : '',
|
||||
brews : []
|
||||
}
|
||||
],
|
||||
navItems : <></>,
|
||||
reportError : null
|
||||
const ListPage = ({ brewCollection = [{ title: '', class: '', brews: [] }], navItems = <></>, reportError = null, query })=>{
|
||||
const [filterString, setFilterString] = useState(query?.filter || '');
|
||||
const [filterTags, setFilterTags] = useState([]);
|
||||
const [sortType, setSortType] = useState(query?.sort || null);
|
||||
const [sortDir, setSortDir] = useState(query?.dir || null);
|
||||
const [groupVisibility, setGroupVisibility] = useState({});
|
||||
|
||||
const groupVisibilityRef = useRef(groupVisibility);
|
||||
const sortTypeRef = useRef(sortType);
|
||||
const sortDirRef = useRef(sortDir);
|
||||
|
||||
useEffect(()=>{
|
||||
groupVisibilityRef.current = groupVisibility;
|
||||
}, [groupVisibility]);
|
||||
|
||||
useEffect(()=>{
|
||||
sortTypeRef.current = sortType;
|
||||
}, [sortType]);
|
||||
|
||||
useEffect(()=>{
|
||||
sortDirRef.current = sortDir;
|
||||
}, [sortDir]);
|
||||
|
||||
useEffect(()=>{
|
||||
window.onbeforeunload = saveToLocalStorage;
|
||||
if(typeof window === 'undefined') return;
|
||||
|
||||
const newSortType = sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE);
|
||||
const newSortDir = sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR);
|
||||
updateUrl(filterString, newSortType, newSortDir);
|
||||
|
||||
const namedBrewCollection = brewCollection.reduce((visibility, brewGroup)=>{
|
||||
visibility[brewGroup.class] = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true') == 'true';
|
||||
return visibility;
|
||||
}, {});
|
||||
|
||||
setGroupVisibility(namedBrewCollection);
|
||||
setSortType(newSortType);
|
||||
setSortDir(newSortDir);
|
||||
|
||||
return ()=>{
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
// HIDE ALL GROUPS UNTIL LOADED
|
||||
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||
brewGroup.visible = false;
|
||||
return brewGroup;
|
||||
}, []);
|
||||
|
||||
const saveToLocalStorage = ()=>{
|
||||
brewCollection.forEach((brewGroup)=>{
|
||||
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${groupVisibilityRef.current[brewGroup.class]}`);
|
||||
});
|
||||
localStorage.setItem(USERPAGE_SORT_TYPE, sortTypeRef.current);
|
||||
localStorage.setItem(USERPAGE_SORT_DIR, sortDirRef.current);
|
||||
};
|
||||
|
||||
return {
|
||||
filterString : this.props.query?.filter || '',
|
||||
filterTags : [],
|
||||
sortType : this.props.query?.sort || null,
|
||||
sortDir : this.props.query?.dir || null,
|
||||
query : this.props.query,
|
||||
brewCollection : brewCollection
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
// SAVE TO LOCAL STORAGE WHEN LEAVING PAGE
|
||||
window.onbeforeunload = this.saveToLocalStorage;
|
||||
|
||||
// LOAD FROM LOCAL STORAGE
|
||||
if(typeof window !== 'undefined') {
|
||||
const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
|
||||
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
|
||||
this.updateUrl(this.state.filterString, newSortType, newSortDir);
|
||||
|
||||
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||
brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
|
||||
return brewGroup;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
brewCollection : brewCollection,
|
||||
sortType : newSortType,
|
||||
sortDir : newSortDir
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
window.onbeforeunload = function(){};
|
||||
},
|
||||
|
||||
saveToLocalStorage : function() {
|
||||
this.state.brewCollection.map((brewGroup)=>{
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
|
||||
});
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
|
||||
},
|
||||
|
||||
renderBrews : function(brews){
|
||||
const renderBrews = (brews)=>{
|
||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||
|
||||
return _.map(brews, (brew, idx)=>{
|
||||
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError} updateListFilter={ (tag)=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}/>;
|
||||
});
|
||||
},
|
||||
return _.map(brews, (brew, idx)=>(
|
||||
<BrewItem
|
||||
brew={brew}
|
||||
key={idx}
|
||||
reportError={reportError}
|
||||
updateListFilter={(tag)=>{
|
||||
updateUrl(filterString, sortType, sortDir, tag);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
sortBrewOrder : function(brew){
|
||||
if(!brew.title){brew.title = 'No Title';}
|
||||
const sortBrewOrder = (brew)=>{
|
||||
const title = brew.title || 'No Title';
|
||||
const mapping = {
|
||||
'alpha' : _.deburr(brew.title.trim().toLowerCase()),
|
||||
'alpha' : _.deburr(title.trim().toLowerCase()),
|
||||
'created' : moment(brew.createdAt).format(),
|
||||
'updated' : moment(brew.updatedAt).format(),
|
||||
'views' : brew.views,
|
||||
'latest' : moment(brew.lastViewed).format()
|
||||
'latest' : moment(brew.lastViewed).format(),
|
||||
};
|
||||
return mapping[this.state.sortType];
|
||||
},
|
||||
return mapping[sortType];
|
||||
};
|
||||
|
||||
handleSortOptionChange : function(event){
|
||||
this.updateUrl(this.state.filterString, event.target.value, this.state.sortDir);
|
||||
this.setState({
|
||||
sortType : event.target.value
|
||||
});
|
||||
},
|
||||
const handleSortOptionChange = (event)=>{
|
||||
updateUrl(filterString, event.target.value, sortDir);
|
||||
setSortType(event.target.value);
|
||||
};
|
||||
|
||||
handleSortDirChange : function(event){
|
||||
const newDir = this.state.sortDir == 'asc' ? 'desc' : 'asc';
|
||||
const handleSortDirChange = (event)=>{
|
||||
const newDir = sortDir == 'asc' ? 'desc' : 'asc';
|
||||
|
||||
this.updateUrl(this.state.filterString, this.state.sortType, newDir);
|
||||
this.setState({
|
||||
sortDir : newDir
|
||||
});
|
||||
},
|
||||
updateUrl(filterString, sortType, newDir);
|
||||
setSortDir(newDir);
|
||||
};
|
||||
|
||||
renderSortOption : function(sortTitle, sortValue){
|
||||
return <div className={`sort-option ${(this.state.sortType == sortValue ? 'active' : '')}`}>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.state.sortType == sortValue ? this.handleSortDirChange : this.handleSortOptionChange}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
{this.state.sortType == sortValue &&
|
||||
<i className={`sortDir fas ${this.state.sortDir == 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`}></i>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
const renderSortOption = (sortTitle, sortValue)=>{
|
||||
return (
|
||||
<div className={`sort-option ${sortType == sortValue ? 'active' : ''}`}>
|
||||
<button value={`${sortValue}`} onClick={sortType == sortValue ? handleSortDirChange : handleSortOptionChange}>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
{sortType == sortValue && <i className={`sortDir fas ${sortDir == 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`}></i>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
handleFilterTextChange : function(e){
|
||||
this.setState({
|
||||
filterString : e.target.value,
|
||||
});
|
||||
this.updateUrl(e.target.value, this.state.sortType, this.state.sortDir);
|
||||
const handleFilterTextChange = (e)=>{
|
||||
setFilterString(e.target.value);
|
||||
updateUrl(e.target.value, sortType, sortDir);
|
||||
return;
|
||||
},
|
||||
};
|
||||
|
||||
updateUrl : function(filterTerm, sortType, sortDir, filterTag=''){
|
||||
const updateUrl = (filterTerm, sortType, sortDir, filterTag = '')=>{
|
||||
const url = new URL(window.location.href);
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
|
||||
@@ -146,135 +131,162 @@ const ListPage = createClass({
|
||||
|
||||
let filterTags = urlParams.getAll('tag');
|
||||
if(filterTag != '') {
|
||||
if(filterTags.findIndex((tag)=>{return tag.toLowerCase()==filterTag.toLowerCase();}) == -1){
|
||||
if(
|
||||
filterTags.findIndex((tag)=>{
|
||||
return tag.toLowerCase() == filterTag.toLowerCase();
|
||||
}) == -1
|
||||
) {
|
||||
filterTags.push(filterTag);
|
||||
} else {
|
||||
filterTags = filterTags.filter((tag)=>{ return tag.toLowerCase() != filterTag.toLowerCase(); });
|
||||
filterTags = filterTags.filter((tag)=>{
|
||||
return tag.toLowerCase() != filterTag.toLowerCase();
|
||||
});
|
||||
}
|
||||
}
|
||||
urlParams.delete('tag');
|
||||
// Add tags to URL in the order they were clicked
|
||||
filterTags.forEach((tag)=>{ urlParams.append('tag', tag); });
|
||||
filterTags.forEach((tag)=>urlParams.append('tag', tag));
|
||||
// Sort tags before updating state
|
||||
filterTags.sort((a, b)=>{
|
||||
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
|
||||
this.setState({
|
||||
filterTags
|
||||
});
|
||||
setFilterTags(filterTags);
|
||||
|
||||
if(!filterTerm)
|
||||
urlParams.delete('filter');
|
||||
else
|
||||
urlParams.set('filter', filterTerm);
|
||||
if(!filterTerm) urlParams.delete('filter');
|
||||
else urlParams.set('filter', filterTerm);
|
||||
|
||||
url.search = urlParams;
|
||||
window.history.replaceState(null, null, url);
|
||||
},
|
||||
};
|
||||
|
||||
renderFilterOption : function(){
|
||||
return <div className='filter-option'>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
type='search'
|
||||
placeholder='filter title/description'
|
||||
onChange={this.handleFilterTextChange}
|
||||
value={this.state.filterString}
|
||||
/>
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
const renderFilterOption = ()=>{
|
||||
return (
|
||||
<div className='filter-option'>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input type='search' placeholder='filter title/description/tags' onChange={handleFilterTextChange} value={filterString} />
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderTagsOptions : function(){
|
||||
if(this.state.filterTags?.length == 0) return;
|
||||
return <div className='tags-container'>
|
||||
{_.map(this.state.filterTags, (tag, idx)=>{
|
||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return <span key={idx} className={matches[1]} onClick={()=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}>{matches[2]}</span>;
|
||||
})}
|
||||
</div>;
|
||||
},
|
||||
const renderTagsOptions = ()=>{
|
||||
if(filterTags?.length == 0) return;
|
||||
return (
|
||||
<div className='tags-container'>
|
||||
{_.map(filterTags, (tag, idx)=>{
|
||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return (
|
||||
<span
|
||||
key={idx}
|
||||
className={matches[1]}
|
||||
onClick={()=>{
|
||||
updateUrl(filterString, sortType, sortDir, tag);
|
||||
}}>
|
||||
{matches[2]}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<h6>Sort by :</h6>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
const renderSortOptions = ()=>{
|
||||
return (
|
||||
<div className='sort-container'>
|
||||
<h6>Sort by :</h6>
|
||||
{renderSortOption('Title', 'alpha')}
|
||||
{renderSortOption('Created Date', 'created')}
|
||||
{renderSortOption('Updated Date', 'updated')}
|
||||
{renderSortOption('Views', 'views')}
|
||||
{/* {renderSortOption('Latest', 'latest')} */}
|
||||
{renderFilterOption()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{this.renderFilterOption()}
|
||||
</div>;
|
||||
},
|
||||
|
||||
getSortedBrews : function(brews){
|
||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||
const getSortedBrews = (brews)=>{
|
||||
const testString = _.deburr(filterString).toLowerCase();
|
||||
|
||||
brews = _.filter(brews, (brew)=>{
|
||||
// Filter by user entered text
|
||||
const brewStrings = _.deburr([
|
||||
brew.title,
|
||||
brew.description,
|
||||
brew.tags].join('\n')
|
||||
.toLowerCase());
|
||||
|
||||
const brewStrings = _.deburr([brew.title, brew.description, brew.tags].join('\n').toLowerCase());
|
||||
const filterTextTest = brewStrings.includes(testString);
|
||||
|
||||
// Filter by user selected tags
|
||||
let filterTagTest = true;
|
||||
if(this.state.filterTags.length > 0){
|
||||
filterTagTest = Array.isArray(brew.tags) && this.state.filterTags?.every((tag)=>{
|
||||
return brew.tags.findIndex((brewTag)=>{
|
||||
return brewTag.toLowerCase() == tag.toLowerCase();
|
||||
}) >= 0;
|
||||
});
|
||||
if(filterTags.length > 0) {
|
||||
filterTagTest =
|
||||
Array.isArray(brew.tags) &&
|
||||
filterTags?.every((tag)=>{
|
||||
return (
|
||||
brew.tags.findIndex((brewTag)=>{
|
||||
return brewTag.toLowerCase() == tag.toLowerCase();
|
||||
}) >= 0
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return filterTextTest && filterTagTest;
|
||||
});
|
||||
|
||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||
},
|
||||
return _.orderBy(
|
||||
brews,
|
||||
(brew)=>{
|
||||
return sortBrewOrder(brew);
|
||||
},
|
||||
sortDir,
|
||||
);
|
||||
};
|
||||
|
||||
toggleBrewCollectionState : function(brewGroupClass) {
|
||||
this.setState((prevState)=>({
|
||||
brewCollection : prevState.brewCollection.map(
|
||||
(brewGroup)=>brewGroup.class === brewGroupClass ? { ...brewGroup, visible: !brewGroup.visible } : brewGroup
|
||||
)
|
||||
}));
|
||||
},
|
||||
const sortedBrewCollection = useMemo(()=>{
|
||||
return brewCollection.map((brewGroup)=>({ ...brewGroup, brews: getSortedBrews(brewGroup.brews) }));
|
||||
}, [brewCollection, filterString, filterTags, sortType, sortDir]);
|
||||
|
||||
renderBrewCollection : function(brewCollection){
|
||||
if(brewCollection == []) return <div className='brewCollection'>
|
||||
<h1>No Brews</h1>
|
||||
</div>;
|
||||
const toggleBrewCollectionState = (brewGroupClass)=>{
|
||||
setGroupVisibility((prevVisibility)=>({ ...prevVisibility, [brewGroupClass]: !prevVisibility[brewGroupClass] }));
|
||||
};
|
||||
|
||||
const renderBrewCollection = (brewCollection)=>{
|
||||
if(brewCollection.length === 0)
|
||||
return (
|
||||
<div className='brewCollection'>
|
||||
<h1>No Brews</h1>
|
||||
</div>
|
||||
);
|
||||
return _.map(brewCollection, (brewGroup, idx)=>{
|
||||
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
||||
<h1 className={brewGroup.visible ? 'active' : 'inactive'} onClick={()=>{this.toggleBrewCollectionState(brewGroup.class);}}>{brewGroup.title || 'No Title'}</h1>
|
||||
{brewGroup.visible ? this.renderBrews(this.getSortedBrews(brewGroup.brews)) : <></>}
|
||||
</div>;
|
||||
});
|
||||
},
|
||||
const sortedBrewGroup = sortedBrewCollection[idx];
|
||||
const visible = groupVisibility[brewGroup.class];
|
||||
|
||||
render : function(){
|
||||
return <div className='listPage sitePage'>
|
||||
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
||||
<link href='/themes/V3/Blank/style.css' type='text/css' rel='stylesheet'/>
|
||||
<link href='/themes/V3/5ePHB/style.css' type='text/css' rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
{this.renderSortOptions()}
|
||||
{this.renderTagsOptions()}
|
||||
return (
|
||||
<div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
||||
<h1
|
||||
className={visible ? 'active' : 'inactive'}
|
||||
onClick={()=>{
|
||||
toggleBrewCollectionState(brewGroup.class);
|
||||
}}>
|
||||
{brewGroup.title || 'No Title'}
|
||||
</h1>
|
||||
{visible ? renderBrews(sortedBrewGroup.brews) : <></>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='listPage sitePage'>
|
||||
<link href='/themes/V3/Blank/style.css' type='text/css' rel='stylesheet' />
|
||||
<link href='/themes/V3/5ePHB/style.css' type='text/css' rel='stylesheet' />
|
||||
{navItems}
|
||||
{renderSortOptions()}
|
||||
{renderTagsOptions()}
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='page'>
|
||||
{this.renderBrewCollection(this.state.brewCollection)}
|
||||
</div>
|
||||
<div className='page'>{renderBrewCollection(brewCollection)}</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ListPage;
|
||||
export default ListPage;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
require('./uiPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
import './uiPage.less';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../../navbar/navbar.jsx');
|
||||
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
|
||||
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../../navbar/account.navitem.jsx');
|
||||
import Nav from '../../../navbar/nav.jsx';
|
||||
import Navbar from '../../../navbar/navbar.jsx';
|
||||
import NewBrewItem from '../../../navbar/newbrew.navitem.jsx';
|
||||
import HelpNavItem from '../../../navbar/help.navitem.jsx';
|
||||
import RecentNavItems from '../../../navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
import Account from '../../../navbar/account.navitem.jsx';
|
||||
|
||||
|
||||
const UIPage = createClass({
|
||||
const UIPage = createReactClass({
|
||||
displayName : 'UIPage',
|
||||
|
||||
render : function(){
|
||||
@@ -35,4 +36,4 @@ const UIPage = createClass({
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UIPage;
|
||||
export default UIPage;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
&::before {
|
||||
margin-right : 5px;
|
||||
font-family : 'Font Awesome 6 Free';
|
||||
font-weight : 900;
|
||||
content : '\f00c';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,499 +1,435 @@
|
||||
/* eslint-disable max-lines */
|
||||
require('./editPage.less');
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const createClass = require('create-react-class');
|
||||
import './editPage.less';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import _ from 'lodash';
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
|
||||
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
import SplitPane from '@components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
import Nav from '@navbar/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 RecentNavItems from '@navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
|
||||
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
// Page specific imports
|
||||
import Headtags from '../../../../vitreum/headtags.js';
|
||||
const Meta = Headtags.Meta;
|
||||
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 { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||
|
||||
const googleDriveIcon = require('../../googleDrive.svg');
|
||||
import googleDriveIcon from '../../googleDrive.svg';
|
||||
|
||||
const SAVE_TIMEOUT = 10000;
|
||||
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
|
||||
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
|
||||
|
||||
const EditPage = createClass({
|
||||
displayName : 'EditPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW_LOAD
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||
alertLoginToTransfer : false,
|
||||
saveGoogle : this.props.brew.googleId ? true : false,
|
||||
confirmGoogleTransfer : false,
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
url : '',
|
||||
autoSave : true,
|
||||
autoSaveWarning : false,
|
||||
unsavedTime : new Date(),
|
||||
currentEditorViewPageNum : 1,
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
displayLockMessage : this.props.brew.lock || false,
|
||||
themeBundle : {}
|
||||
};
|
||||
},
|
||||
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
|
||||
const BREWKEY = 'HB_newPage_content';
|
||||
const STYLEKEY = 'HB_newPage_style';
|
||||
const SNIPKEY = 'HB_newPage_snippets';
|
||||
const METAKEY = 'HB_newPage_meta';
|
||||
|
||||
editor : React.createRef(null),
|
||||
savedBrew : null,
|
||||
const useLocalStorage = false;
|
||||
const neverSaved = false;
|
||||
|
||||
componentDidMount : function(){
|
||||
this.setState({
|
||||
url : window.location.href
|
||||
});
|
||||
const EditPage = (props)=>{
|
||||
props = {
|
||||
brew : DEFAULT_BREW_LOAD,
|
||||
...props
|
||||
};
|
||||
|
||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||
const [currentBrew, setCurrentBrew] = useState(props.brew);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [lastSavedTime, setLastSavedTime] = useState(new Date());
|
||||
const [saveGoogle, setSaveGoogle] = useState(!!props.brew.googleId);
|
||||
const [error, setError] = useState(null);
|
||||
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text));
|
||||
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
|
||||
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||
const [themeBundle, setThemeBundle] = useState({});
|
||||
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
||||
const [alertTrashedGoogleBrew, setAlertTrashedGoogleBrew] = useState(props.brew.trashed);
|
||||
const [alertNoGoogleToTransfer, setAlertNoGoogleToTransfer] = useState(false);
|
||||
const [alertOwnershipToTransfer, setAlertOwnershipToTransfer] = useState(false);
|
||||
const [confirmGoogleTransfer, setConfirmGoogleTransfer] = useState(false);
|
||||
const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
|
||||
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
|
||||
|
||||
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
||||
if(this.state.autoSave){
|
||||
this.trySave();
|
||||
} else {
|
||||
this.setState({ autoSaveWarning: true });
|
||||
const editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
const saveTimeout = useRef(null);
|
||||
const warnUnsavedTimeout = useRef(null);
|
||||
const trySaveRef = useRef(null); // 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
|
||||
|
||||
useEffect(()=>{
|
||||
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
|
||||
setAutoSaveEnabled(autoSavePref);
|
||||
setWarnUnsavedChanges(!autoSavePref);
|
||||
setHTMLErrors(hbfm.validate(currentBrew.text));
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
if(e.keyCode === 83) trySaveRef.current(true, true, saveGoogle);
|
||||
if(e.keyCode === 80) printCurrentBrew();
|
||||
if([83, 80].includes(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
if(unsavedChangesRef.current)
|
||||
return 'You have unsaved changes!';
|
||||
}
|
||||
};
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
useEffect(()=>{
|
||||
trySaveRef.current = trySave;
|
||||
unsavedChangesRef.current = unsavedChanges;
|
||||
});
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.onbeforeunload = function(){};
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentDidUpdate : function(){
|
||||
const hasChange = this.hasChanges();
|
||||
if(this.state.isPending != hasChange){
|
||||
this.setState({
|
||||
isPending : hasChange
|
||||
});
|
||||
if(autoSaveEnabled) trySave(false, hasChange, saveGoogle);
|
||||
}, [currentBrew]);
|
||||
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current?.update();
|
||||
};
|
||||
|
||||
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||
if(subfield == 'renderer' || subfield == 'theme')
|
||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||
|
||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||
setHTMLErrors(hbfm.validate(value));
|
||||
|
||||
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : value.renderer,
|
||||
theme : value.theme,
|
||||
lang : value.lang
|
||||
}));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
handleControlKeys : function(e){
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.trySave(true);
|
||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
|
||||
...prevBrew,
|
||||
style : newData.style,
|
||||
text : newData.text,
|
||||
snippets : newData.snippets
|
||||
}));
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.editor.current.update();
|
||||
},
|
||||
const resetWarnUnsavedTimer = ()=>{
|
||||
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
|
||||
clearTimeout(warnUnsavedTimeout.current);
|
||||
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
|
||||
};
|
||||
|
||||
handleEditorViewPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||
},
|
||||
|
||||
handleEditorCursorPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||
},
|
||||
|
||||
handleBrewRendererPageChange : function(pageNumber){
|
||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style }
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
...metadata
|
||||
}
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
hasChanges : function(){
|
||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||
},
|
||||
|
||||
updateBrew : function(newData){
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
style : newData.style,
|
||||
text : newData.text
|
||||
}
|
||||
}));
|
||||
},
|
||||
|
||||
trySave : function(immediate=false){
|
||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
if(this.hasChanges()){
|
||||
this.debounceSave();
|
||||
} else {
|
||||
this.debounceSave.cancel();
|
||||
const handleGoogleClick = ()=>{
|
||||
if(global.account !== currentBrew.authors[0]) {
|
||||
setalertOwnershipToTransfer(true);
|
||||
return;
|
||||
}
|
||||
if(immediate) this.debounceSave.flush();
|
||||
},
|
||||
|
||||
handleGoogleClick : function(){
|
||||
if(!global.account?.googleId) {
|
||||
this.setState({
|
||||
alertLoginToTransfer : true
|
||||
});
|
||||
setAlertNoGoogleToTransfer(true);
|
||||
return;
|
||||
}
|
||||
this.setState((prevState)=>({
|
||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||
}));
|
||||
this.setState({
|
||||
error : null,
|
||||
isSaving : false
|
||||
});
|
||||
},
|
||||
|
||||
closeAlerts : function(event){
|
||||
event.stopPropagation(); //Only handle click once so alert doesn't reopen
|
||||
this.setState({
|
||||
alertTrashedGoogleBrew : false,
|
||||
alertLoginToTransfer : false,
|
||||
confirmGoogleTransfer : false
|
||||
});
|
||||
},
|
||||
setConfirmGoogleTransfer((prev)=>!prev);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
toggleGoogleStorage : function(){
|
||||
this.setState((prevState)=>({
|
||||
saveGoogle : !prevState.saveGoogle,
|
||||
isSaving : false,
|
||||
error : null
|
||||
}), ()=>this.save());
|
||||
},
|
||||
const closeAlerts = (e)=>{
|
||||
e.stopPropagation(); //Only handle click once so alert doesn't reopen
|
||||
setAlertTrashedGoogleBrew(false);
|
||||
setAlertNoGoogleToTransfer(false);
|
||||
setConfirmGoogleTransfer(false);
|
||||
setalertOwnershipToTransfer(false);
|
||||
};
|
||||
|
||||
save : async function(){
|
||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||
const toggleGoogleStorage = (e)=>{
|
||||
closeAlerts(e);
|
||||
const newSaveGoogle = !saveGoogle;
|
||||
setSaveGoogle((prev)=>!prev);
|
||||
setError(null);
|
||||
trySave(true, true, newSaveGoogle);
|
||||
};
|
||||
|
||||
this.setState((prevState)=>({
|
||||
isSaving : true,
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
const trySave = (immediate = false, hasChanges = true, saveToGoogle = false)=>{
|
||||
clearTimeout(saveTimeout.current);
|
||||
if(isSaving) return;
|
||||
if(!hasChanges && !immediate) return;
|
||||
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
|
||||
|
||||
await updateHistory(this.state.brew).catch(console.error);
|
||||
saveTimeout.current = setTimeout(async ()=>{
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
await save(currentBrew, saveToGoogle)
|
||||
.catch((err)=>{
|
||||
setError(err);
|
||||
});
|
||||
setIsSaving(false);
|
||||
setLastSavedTime(new Date());
|
||||
if(!autoSaveEnabled) resetWarnUnsavedTimer();
|
||||
}, newTimeout);
|
||||
};
|
||||
|
||||
const save = async (brew, saveToGoogle)=>{
|
||||
setHTMLErrors(hbfm.validate(brew.text));
|
||||
|
||||
await updateHistory(brew).catch(console.error);
|
||||
await versionHistoryGarbageCollection().catch(console.error);
|
||||
|
||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||
//Prepare content to send to server
|
||||
const brewToSave = {
|
||||
...brew,
|
||||
text : brew.text.normalize('NFC'),
|
||||
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1,
|
||||
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
|
||||
hash : await md5(lastSavedBrew.current.text.normalize('NFC')),
|
||||
textBin : undefined,
|
||||
version : lastSavedBrew.current.version
|
||||
};
|
||||
|
||||
const brew = this.state.brew;
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave)));
|
||||
const transfer = saveToGoogle === _.isNil(brew.googleId);
|
||||
const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : '';
|
||||
|
||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}${params}`)
|
||||
.send(brew)
|
||||
.put(`/api/update/${brewToSave.editId}${params}`)
|
||||
.set('Content-Encoding', 'gzip')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(compressedBrew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ error: err });
|
||||
console.error('Error Updating Local Brew');
|
||||
setError(err);
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
this.savedBrew = {
|
||||
...this.state.brew,
|
||||
googleId : res.body.googleId ? res.body.googleId : null,
|
||||
editId : res.body.editId,
|
||||
const updatedFields = {
|
||||
googleId : res.body.googleId ?? null,
|
||||
editId : res.body.editId,
|
||||
shareId : res.body.shareId,
|
||||
version : res.body.version
|
||||
};
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
|
||||
this.setState(()=>({
|
||||
brew : this.savedBrew,
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
unsavedTime : new Date()
|
||||
lastSavedBrew.current = {
|
||||
...brew,
|
||||
...updatedFields
|
||||
};
|
||||
|
||||
setCurrentBrew((prevBrew)=>({
|
||||
...prevBrew,
|
||||
...updatedFields
|
||||
}));
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
|
||||
history.replaceState(null, null, `/edit/${res.body.editId}`);
|
||||
};
|
||||
|
||||
{this.state.confirmGoogleTransfer &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
{ this.state.saveGoogle
|
||||
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
|
||||
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
|
||||
}
|
||||
<br />
|
||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
||||
Yes
|
||||
</div>
|
||||
<div className='deny'>
|
||||
No
|
||||
</div>
|
||||
const renderGoogleDriveIcon = ()=>(
|
||||
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}>
|
||||
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' />
|
||||
|
||||
|
||||
|
||||
{alertOwnershipToTransfer && (
|
||||
<div className='errorContainer'>
|
||||
You must be the Owner to transfer between the Homebrewery and Google Drive!
|
||||
The owner of this file is {currentBrew.authors[0]}
|
||||
<div className='confirm' onClick={closeAlerts}> Okay </div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{this.state.alertLoginToTransfer &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
You must be signed in to a Google account to transfer
|
||||
between the homebrewery and Google Drive!
|
||||
<a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||
<div className='confirm'>
|
||||
Sign In
|
||||
</div>
|
||||
{alertNoGoogleToTransfer && (
|
||||
<div className='errorContainer'>
|
||||
You must be signed in to a Google account to transfer between the Homebrewery and Google Drive!
|
||||
<a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||
<div className='confirm' onClick={closeAlerts}> Sign In </div>
|
||||
</a>
|
||||
<div className='deny'>
|
||||
Not Now
|
||||
</div>
|
||||
<div className='deny' onClick={closeAlerts}> Not Now </div>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{this.state.alertTrashedGoogleBrew &&
|
||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm'>
|
||||
OK
|
||||
</div>
|
||||
{alertTrashedGoogleBrew && (
|
||||
<div className='errorContainer'>
|
||||
This brew is currently in your Trash folder on Google Drive!<br />
|
||||
If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||
<div className='confirm' onClick={toggleGoogleStorage}> Save my brew </div>
|
||||
</div>
|
||||
}
|
||||
</Nav.item>;
|
||||
},
|
||||
)}
|
||||
|
||||
renderSaveButton : function(){
|
||||
{confirmGoogleTransfer && (
|
||||
<div className='errorContainer'>
|
||||
{saveGoogle
|
||||
? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?'
|
||||
: 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
|
||||
<br />
|
||||
<div className='confirm' onClick={toggleGoogleStorage}> Yes </div>
|
||||
<div className='deny' onClick={closeAlerts}> No </div>
|
||||
</div>
|
||||
)}
|
||||
</Nav.item>
|
||||
);
|
||||
|
||||
const renderSaveButton = ()=>{
|
||||
// #1 - Currently saving, show SAVING
|
||||
if(this.state.isSaving){
|
||||
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(this.state.isPending && this.state.autoSaveWarning){
|
||||
this.setAutosaveWarning();
|
||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
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>
|
||||
Reminder...
|
||||
<div className='errorContainer'>{text}</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||
// Use trySave(true) instead of save() to use debounced save function
|
||||
if(this.state.isPending){
|
||||
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
if(unsavedChanges)
|
||||
return <Nav.item className='save' onClick={()=>trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now</Nav.item>;
|
||||
|
||||
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||
if(this.state.autoSave){
|
||||
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
||||
}
|
||||
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' disabled={true}>save now</Nav.item>;
|
||||
|
||||
// DEFAULT - No unsaved changes, show SAVED
|
||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||
},
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
handleAutoSave : function(){
|
||||
if(this.warningTimer) clearTimeout(this.warningTimer);
|
||||
this.setState((prevState)=>({
|
||||
autoSave : !prevState.autoSave,
|
||||
autoSaveWarning : prevState.autoSave
|
||||
}), ()=>{
|
||||
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
|
||||
});
|
||||
},
|
||||
const toggleAutoSave = ()=>{
|
||||
clearTimeout(warnUnsavedTimeout.current);
|
||||
clearTimeout(saveTimeout.current);
|
||||
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
|
||||
setAutoSaveEnabled(!autoSaveEnabled);
|
||||
setWarnUnsavedChanges(autoSaveEnabled);
|
||||
};
|
||||
|
||||
setAutosaveWarning : function(){
|
||||
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
|
||||
this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
|
||||
this.warningTimer;
|
||||
},
|
||||
const renderAutoSaveButton = ()=>(
|
||||
<Nav.item onClick={toggleAutoSave}>
|
||||
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||
</Nav.item>
|
||||
);
|
||||
|
||||
errorReported : function(error) {
|
||||
this.setState({
|
||||
error
|
||||
});
|
||||
},
|
||||
|
||||
renderAutoSaveButton : function(){
|
||||
return <Nav.item onClick={this.handleAutoSave}>
|
||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
processShareId : function() {
|
||||
return this.state.brew.googleId && !this.state.brew.stubbed ?
|
||||
this.state.brew.googleId + this.state.brew.shareId :
|
||||
this.state.brew.shareId;
|
||||
},
|
||||
|
||||
getRedditLink : function(){
|
||||
|
||||
const shareLink = this.processShareId();
|
||||
const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
|
||||
const title = `${this.props.brew.title} ${systems}`;
|
||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||
|
||||
**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
|
||||
|
||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
const shareLink = this.processShareId();
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>{
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
<Nav.dropdown className='save-menu'>
|
||||
{this.renderSaveButton()}
|
||||
{this.renderAutoSaveButton()}
|
||||
</Nav.dropdown>
|
||||
}
|
||||
<NewBrew />
|
||||
<HelpNavItem/>
|
||||
<Nav.dropdown>
|
||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||
share
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
||||
view
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
|
||||
copy url
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||
post to reddit
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
{renderGoogleDriveIcon()}
|
||||
{error
|
||||
? <ErrorNavItem error={error} clearError={clearError} />
|
||||
: <Nav.dropdown className='save-menu'>
|
||||
{renderSaveButton()}
|
||||
{renderAutoSaveButton()}
|
||||
</Nav.dropdown>}
|
||||
<NewBrewItem />
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||
<Account />
|
||||
<ShareNavItem brew={currentBrew} currentPage={currentBrewRendererPageNum} />
|
||||
<RecentNavItem brew={currentBrew} storageKey='edit' />
|
||||
<AccountNavItem/>
|
||||
</Nav.section>
|
||||
|
||||
</Navbar>;
|
||||
},
|
||||
};
|
||||
|
||||
render : function(){
|
||||
return <div className='editPage sitePage'>
|
||||
return (
|
||||
<div className='editPage sitePage'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
|
||||
{renderNavbar()}
|
||||
|
||||
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<SplitPane onDragFinish={handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
reportError={this.errorReported}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
updateBrew={this.updateBrew}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
ref={editorRef}
|
||||
brew={currentBrew}
|
||||
onBrewChange={handleBrewChange}
|
||||
reportError={setError}
|
||||
renderer={currentBrew.renderer}
|
||||
userThemes={props.userThemes}
|
||||
themeBundle={themeBundle}
|
||||
updateBrew={updateBrew}
|
||||
onCursorPageChange={setCurrentEditorCursorPageNum}
|
||||
onViewPageChange={setCurrentEditorViewPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
theme={currentBrew.theme}
|
||||
themeBundle={themeBundle}
|
||||
errors={HTMLErrors}
|
||||
lang={currentBrew.lang}
|
||||
onPageChange={setCurrentBrewRendererPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = EditPage;
|
||||
export default EditPage;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './lockNotification.less';
|
||||
import * as React from 'react';
|
||||
import request from '../../../utils/request-middleware.js';
|
||||
import Dialog from '../../../../components/dialog.jsx';
|
||||
import Dialog from '@components/dialog.jsx';
|
||||
|
||||
function LockNotification(props) {
|
||||
props = {
|
||||
@@ -40,4 +40,4 @@ function LockNotification(props) {
|
||||
</Dialog>;
|
||||
};
|
||||
|
||||
module.exports = LockNotification;
|
||||
export default LockNotification;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
require('./errorPage.less');
|
||||
const React = require('react');
|
||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||
import Markdown from '../../../../shared/naturalcrit/markdown.js';
|
||||
const ErrorIndex = require('./errors/errorIndex.js');
|
||||
import './errorPage.less';
|
||||
import React from 'react';
|
||||
import UIPage from '../basePages/uiPage/uiPage.jsx';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import ErrorIndex from './errors/errorIndex.js';
|
||||
|
||||
const ErrorPage = ({ brew })=>{
|
||||
// Retrieving the error text based on the brew's error code from ErrorIndex
|
||||
@@ -16,10 +16,10 @@ const ErrorPage = ({ brew })=>{
|
||||
<h4>{brew?.text || 'No error text'}</h4>
|
||||
</div>
|
||||
<hr />
|
||||
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: hbfm.render(errorText) }} />
|
||||
</div>
|
||||
</UIPage>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ErrorPage;
|
||||
export default ErrorPage;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.homebrew {
|
||||
.uiPage.sitePage {
|
||||
.uiPage.sitePage:has(.errorTitle) {
|
||||
.errorTitle {
|
||||
//background-color: @orange;
|
||||
color : #D02727;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const dedent = require('dedent-tabs').default;
|
||||
import dedent from 'dedent';
|
||||
|
||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||
|
||||
@@ -96,7 +96,7 @@ const errorIndex = (props)=>{
|
||||
|
||||
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${encodeURIComponent(author)})`;}).join(', ') || 'Unable to list authors'}
|
||||
|
||||
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
|
||||
|
||||
@@ -111,7 +111,7 @@ const errorIndex = (props)=>{
|
||||
|
||||
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${encodeURIComponent(author)})`;}).join(', ') || 'Unable to list authors'}
|
||||
|
||||
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
|
||||
|
||||
@@ -176,6 +176,32 @@ const errorIndex = (props)=>{
|
||||
|
||||
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
||||
|
||||
// ID validation error
|
||||
'11' : dedent`
|
||||
## No Homebrewery document could be found.
|
||||
|
||||
The server could not locate the Homebrewery document. The Brew ID failed the validation check.
|
||||
|
||||
:
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Google ID validation error
|
||||
'12' : dedent`
|
||||
## No Google document could be found.
|
||||
|
||||
The server could not locate the Google document. The Google ID failed the validation check.
|
||||
|
||||
:
|
||||
|
||||
**Brew ID:** ${props.brew.brewId}`,
|
||||
|
||||
// Database Connection Lost
|
||||
'13' : dedent`
|
||||
## Database connection has been lost.
|
||||
|
||||
The server could not communicate with the database.`,
|
||||
|
||||
//account page when account is not defined
|
||||
'50' : dedent`
|
||||
## You are not signed in
|
||||
@@ -196,7 +222,7 @@ const errorIndex = (props)=>{
|
||||
|
||||
**Brew Title:** ${escape(props.brew.brewTitle)}
|
||||
|
||||
**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`,
|
||||
**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${encodeURIComponent(author)})`;}).join(', ') || 'Unable to list authors'}`,
|
||||
|
||||
// ####### Admin page error #######
|
||||
'52' : dedent`
|
||||
@@ -242,4 +268,4 @@ const errorIndex = (props)=>{
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = errorIndex;
|
||||
export default errorIndex;
|
||||
|
||||
@@ -1,141 +1,236 @@
|
||||
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');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
import './homePage.less';
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import _ from 'lodash';
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
|
||||
|
||||
const HomePage = createClass({
|
||||
displayName : 'HomePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0'
|
||||
import SplitPane from '@components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
import Nav from '@navbar/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 RecentNavItems from '@navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
|
||||
|
||||
// Page specific imports
|
||||
import Headtags from '@vitreum/headtags.js';
|
||||
const Meta = Headtags.Meta;
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const SNIPKEY = 'homebrewery-new-snippets';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const useLocalStorage = false;
|
||||
const neverSaved = true;
|
||||
|
||||
const HomePage =(props)=>{
|
||||
props = {
|
||||
brew : DEFAULT_BREW,
|
||||
ver : '0.0.0',
|
||||
...props
|
||||
};
|
||||
|
||||
const [currentBrew, setCurrentBrew] = useState(props.brew);
|
||||
const [error, setError] = useState(undefined);
|
||||
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text));
|
||||
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
|
||||
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||
const [themeBundle, setThemeBundle] = useState({});
|
||||
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [autoSaveEnabled, setAutoSaveEnable] = useState(false);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
const unsavedChangesRef = useRef(unsavedChanges);
|
||||
|
||||
useEffect(()=>{
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
if(e.keyCode === 83) trySaveRef.current(true);
|
||||
if(e.keyCode === 80) printCurrentBrew();
|
||||
if([83, 80].includes(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
welcomeText : this.props.brew.text,
|
||||
error : undefined,
|
||||
currentEditorViewPageNum : 1,
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
themeBundle : {}
|
||||
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(unsavedChangesRef.current)
|
||||
return 'You have unsaved changes!';
|
||||
};
|
||||
},
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
window.onbeforeunload = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
editor : React.createRef(null),
|
||||
useEffect(()=>{
|
||||
unsavedChangesRef.current = unsavedChanges;
|
||||
}, [unsavedChanges]);
|
||||
|
||||
componentDidMount : function() {
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
},
|
||||
|
||||
handleSave : function(){
|
||||
const save = ()=>{
|
||||
request.post('/api')
|
||||
.send(this.state.brew)
|
||||
.send(currentBrew)
|
||||
.end((err, res)=>{
|
||||
if(err) {
|
||||
this.setState({ error: err });
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
const brew = res.body;
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
const saved = res.body;
|
||||
window.location = `/edit/${saved.editId}`;
|
||||
});
|
||||
},
|
||||
handleSplitMove : function(){
|
||||
this.editor.current.update();
|
||||
},
|
||||
};
|
||||
|
||||
handleEditorViewPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||
},
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
|
||||
handleEditorCursorPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||
},
|
||||
if(autoSaveEnabled) trySave(false, hasChange);
|
||||
}, [currentBrew]);
|
||||
|
||||
handleBrewRendererPageChange : function(pageNumber){
|
||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||
},
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current.update();
|
||||
};
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
}));
|
||||
},
|
||||
renderNavbar : function(){
|
||||
return <Navbar ver={this.props.ver}>
|
||||
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||
if(subfield == 'renderer' || subfield == 'theme')
|
||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||
|
||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||
setHTMLErrors(hbfm.validate(value));
|
||||
|
||||
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : value.renderer,
|
||||
theme : value.theme,
|
||||
lang : value.lang
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
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' disabled={true}>save now</Nav.item>;
|
||||
|
||||
// DEFAULT - No unsaved changes, show SAVED
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>{
|
||||
return <Navbar ver={props.ver}>
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
null
|
||||
}
|
||||
{error
|
||||
? <ErrorNavItem error={error} clearError={clearError} />
|
||||
: renderSaveButton()}
|
||||
<NewBrewItem />
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
};
|
||||
|
||||
render : function(){
|
||||
return <div className='homePage sitePage'>
|
||||
return (
|
||||
<div className='homePage sitePage'>
|
||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||
{this.renderNavbar()}
|
||||
{renderNavbar()}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<SplitPane onDragFinish={handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
ref={editorRef}
|
||||
brew={currentBrew}
|
||||
onBrewChange={handleBrewChange}
|
||||
renderer={currentBrew.renderer}
|
||||
showEditButtons={false}
|
||||
themeBundle={this.state.themeBundle}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
themeBundle={themeBundle}
|
||||
onCursorPageChange={setCurrentEditorCursorPageNum}
|
||||
onViewPageChange={setCurrentEditorViewPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
themeBundle={this.state.themeBundle}
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
onPageChange={setCurrentBrewRendererPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
themeBundle={themeBundle}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
||||
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
|
||||
Save current <i className='fas fa-save' />
|
||||
</div>
|
||||
|
||||
<a href='/new' className='floatingNewButton'>
|
||||
Create your own <i className='fas fa-magic' />
|
||||
</a>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = HomePage;
|
||||
export default HomePage;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '@sharedStyles/core.less';
|
||||
|
||||
.homePage {
|
||||
position : relative;
|
||||
a.floatingNewButton {
|
||||
@@ -35,6 +37,14 @@
|
||||
|
||||
.navItem.save {
|
||||
background-color : @orange;
|
||||
transition:all 0.2s;
|
||||
&:hover { background-color : @green; }
|
||||
|
||||
&.neverSaved {
|
||||
translate:-100%;
|
||||
opacity: 0;
|
||||
background-color :#333;
|
||||
cursor:auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
|
||||
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
||||
}}
|
||||
|
||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||
|
||||
{{artist,bottom:160px,left:100px
|
||||
##### Homebrew Mug
|
||||
@@ -77,16 +77,16 @@ If you wish to sell or in some way gain profit for what's created on this site,
|
||||
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
### More Homebrew Resources
|
||||
[{width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
|
||||
[{width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
|
||||
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
|
||||
{{position:absolute;top:20px;right:20px;width:auto
|
||||
[{height:30px}](https://discord.gg/by3deKx)
|
||||
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||
[{height:30px}](https://discord.gg/by3deKx)
|
||||
[{height:30px}](https://github.com/naturalcrit/homebrewery)
|
||||
[{height:30px}](https://patreon.com/NaturalCrit)
|
||||
[{height:30px}](https://www.reddit.com/r/homebrewery/)
|
||||
}}
|
||||
|
||||
\page
|
||||
@@ -162,7 +162,7 @@ Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You
|
||||
|
||||
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
||||
|
||||
 {width:100px,border:"2px solid",border-radius:10px}
|
||||
 {width:100px,border:"2px solid",border-radius:10px}
|
||||
|
||||
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.*
|
||||
|
||||
|
||||
@@ -1,276 +1,279 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./newPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
import request from '../../utils/request-middleware.js';
|
||||
/* eslint-disable max-lines */
|
||||
import './newPage.less';
|
||||
|
||||
import Markdown from 'naturalcrit/markdown.js';
|
||||
// Common imports
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import { hbfm } from 'hbmarkedwrapper';
|
||||
import _ from 'lodash';
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
import SplitPane from '@components/splitPane/splitPane.jsx';
|
||||
import Editor from '../../editor/editor.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
import Nav from '@navbar/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 RecentNavItems from '@navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
let SAVEKEY;
|
||||
// Page specific imports
|
||||
|
||||
const BREWKEY = 'HB_newPage_content';
|
||||
const STYLEKEY = 'HB_newPage_style';
|
||||
const METAKEY = 'HB_newPage_metadata';
|
||||
const SNIPKEY = 'HB_newPage_snippets';
|
||||
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
|
||||
|
||||
const NewPage = createClass({
|
||||
displayName : 'NewPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : DEFAULT_BREW
|
||||
const useLocalStorage = true;
|
||||
const neverSaved = true;
|
||||
|
||||
const NewPage = (props)=>{
|
||||
props = {
|
||||
brew : DEFAULT_BREW,
|
||||
...props
|
||||
};
|
||||
|
||||
const [currentBrew, setCurrentBrew] = useState(props.brew);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
|
||||
const [error, setError] = useState(null);
|
||||
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text));
|
||||
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
|
||||
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||
const [themeBundle, setThemeBundle] = useState({});
|
||||
const [unsavedChanges, setUnsavedChanges] = useState(false);
|
||||
const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||
// const saveTimeout = useRef(null);
|
||||
// const warnUnsavedTimeout = useRef(null);
|
||||
const trySaveRef = useRef(null); // 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
|
||||
|
||||
useEffect(()=>{
|
||||
loadBrew();
|
||||
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
if(e.keyCode === 83) trySaveRef.current(true);
|
||||
if(e.keyCode === 80) printCurrentBrew();
|
||||
if([83, 80].includes(e.keyCode)) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
const brew = this.props.brew;
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
|
||||
return {
|
||||
brew : brew,
|
||||
isSaving : false,
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
error : null,
|
||||
htmlErrors : Markdown.validate(brew.text),
|
||||
currentEditorViewPageNum : 1,
|
||||
currentEditorCursorPageNum : 1,
|
||||
currentBrewRendererPageNum : 1,
|
||||
themeBundle : {}
|
||||
return ()=>{
|
||||
document.removeEventListener('keydown', handleControlKeys);
|
||||
};
|
||||
},
|
||||
}, []);
|
||||
|
||||
editor : React.createRef(null),
|
||||
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
const brew = this.state.brew;
|
||||
|
||||
if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||
const loadBrew = ()=>{
|
||||
const brew = { ...currentBrew };
|
||||
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||
const brewStorage = localStorage.getItem(BREWKEY);
|
||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
|
||||
brew.text = brewStorage ?? brew.text;
|
||||
brew.style = styleStorage ?? brew.style;
|
||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||
brew.text = brewStorage ?? brew.text;
|
||||
brew.style = styleStorage ?? brew.style;
|
||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||
}
|
||||
|
||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
||||
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
|
||||
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||
|
||||
this.setState({
|
||||
brew : brew,
|
||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||
});
|
||||
|
||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
||||
setCurrentBrew(brew);
|
||||
lastSavedBrew.current = brew;
|
||||
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
if(brew.style)
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||
if(window.location.pathname != '/new') {
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang }));
|
||||
if(window.location.pathname !== '/new')
|
||||
window.history.replaceState({}, window.location.title, '/new/');
|
||||
}
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
};
|
||||
|
||||
handleControlKeys : function(e){
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.save();
|
||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
useEffect(()=>{
|
||||
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||
setUnsavedChanges(hasChange);
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.editor.current.update();
|
||||
},
|
||||
if(autoSaveEnabled) trySave(false, hasChange);
|
||||
}, [currentBrew]);
|
||||
|
||||
handleEditorViewPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
||||
},
|
||||
useEffect(()=>{
|
||||
trySaveRef.current = trySave;
|
||||
unsavedChangesRef.current = unsavedChanges;
|
||||
});
|
||||
|
||||
handleEditorCursorPageChange : function(pageNumber){
|
||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
||||
},
|
||||
const handleSplitMove = ()=>{
|
||||
editorRef.current.update();
|
||||
};
|
||||
|
||||
handleBrewRendererPageChange : function(pageNumber){
|
||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
||||
},
|
||||
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
|
||||
if(subfield == 'renderer' || subfield == 'theme')
|
||||
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||
|
||||
handleTextChange : function(text){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||
setHTMLErrors(hbfm.validate(value));
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text },
|
||||
htmlErrors : htmlErrors,
|
||||
}));
|
||||
localStorage.setItem(BREWKEY, text);
|
||||
},
|
||||
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
|
||||
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style },
|
||||
}));
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
|
||||
handleSnipChange : function(snippet){
|
||||
//If there are errors, run the validator on every change to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, snippets: snippet },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors,
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata, field=undefined){
|
||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, ...metadata },
|
||||
}), ()=>{
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
// 'title' : this.state.brew.title,
|
||||
// 'description' : this.state.brew.description,
|
||||
'renderer' : this.state.brew.renderer,
|
||||
'theme' : this.state.brew.theme,
|
||||
'lang' : this.state.brew.lang
|
||||
if(useLocalStorage) {
|
||||
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||
renderer : value.renderer,
|
||||
theme : value.theme,
|
||||
lang : value.lang
|
||||
}));
|
||||
});
|
||||
;
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
this.setState({
|
||||
isSaving : true
|
||||
});
|
||||
|
||||
let brew = this.state.brew;
|
||||
// Split out CSS to Style if CSS codefence exists
|
||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
const trySave = async ()=>{
|
||||
setIsSaving(true);
|
||||
|
||||
const updatedBrew = { ...currentBrew };
|
||||
splitTextStyleAndMetadata(updatedBrew);
|
||||
|
||||
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm;
|
||||
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
||||
|
||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
const res = await request
|
||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(brew)
|
||||
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(updatedBrew)
|
||||
.catch((err)=>{
|
||||
this.setState({ isSaving: false, error: err });
|
||||
setIsSaving(false);
|
||||
setError(err);
|
||||
});
|
||||
|
||||
setIsSaving(false);
|
||||
if(!res) return;
|
||||
|
||||
brew = res.body;
|
||||
const savedBrew = res.body;
|
||||
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
},
|
||||
window.location = `/edit/${savedBrew.editId}`;
|
||||
};
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||
save...
|
||||
</Nav.item>;
|
||||
} else {
|
||||
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
|
||||
save
|
||||
</Nav.item>;
|
||||
}
|
||||
},
|
||||
const renderSaveButton = ()=>{
|
||||
// #1 - Currently saving, show SAVING
|
||||
if(isSaving)
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
// #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={trySave} 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' disabled={true}>save now</Nav.item>;
|
||||
|
||||
// DEFAULT - No unsaved changes, show SAVED
|
||||
return <Nav.item className='save saved'>saved</Nav.item>;
|
||||
};
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const renderNavbar = ()=>(
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.state.error ?
|
||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||
this.renderSaveButton()
|
||||
}
|
||||
{error
|
||||
? <ErrorNavItem error={error} clearError={clearError} />
|
||||
: renderSaveButton()}
|
||||
<NewBrewItem />
|
||||
<PrintNavItem />
|
||||
<HelpNavItem />
|
||||
<VaultNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
</Navbar>
|
||||
);
|
||||
|
||||
render : function(){
|
||||
return <div className='newPage sitePage'>
|
||||
{this.renderNavbar()}
|
||||
return (
|
||||
<div className='newPage sitePage'>
|
||||
{renderNavbar()}
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||
<SplitPane onDragFinish={handleSplitMove}>
|
||||
<Editor
|
||||
ref={this.editor}
|
||||
brew={this.state.brew}
|
||||
onTextChange={this.handleTextChange}
|
||||
onStyleChange={this.handleStyleChange}
|
||||
onMetaChange={this.handleMetaChange}
|
||||
onSnipChange={this.handleSnipChange}
|
||||
renderer={this.state.brew.renderer}
|
||||
userThemes={this.props.userThemes}
|
||||
themeBundle={this.state.themeBundle}
|
||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
||||
onViewPageChange={this.handleEditorViewPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
ref={editorRef}
|
||||
brew={currentBrew}
|
||||
onBrewChange={handleBrewChange}
|
||||
renderer={currentBrew.renderer}
|
||||
userThemes={props.userThemes}
|
||||
themeBundle={themeBundle}
|
||||
onCursorPageChange={setCurrentEditorCursorPageNum}
|
||||
onViewPageChange={setCurrentEditorViewPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
/>
|
||||
<BrewRenderer
|
||||
text={this.state.brew.text}
|
||||
style={this.state.brew.style}
|
||||
renderer={this.state.brew.renderer}
|
||||
theme={this.state.brew.theme}
|
||||
themeBundle={this.state.themeBundle}
|
||||
errors={this.state.htmlErrors}
|
||||
lang={this.state.brew.lang}
|
||||
onPageChange={this.handleBrewRendererPageChange}
|
||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
||||
text={currentBrew.text}
|
||||
style={currentBrew.style}
|
||||
renderer={currentBrew.renderer}
|
||||
theme={currentBrew.theme}
|
||||
themeBundle={themeBundle}
|
||||
errors={HTMLErrors}
|
||||
lang={currentBrew.lang}
|
||||
onPageChange={setCurrentBrewRendererPageNum}
|
||||
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = NewPage;
|
||||
export default NewPage;
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
@import '@sharedStyles/colors.less';
|
||||
|
||||
.newPage {
|
||||
.navItem.save {
|
||||
background-color : @orange;
|
||||
transition:all 0.2s;
|
||||
&:hover { background-color : @green; }
|
||||
|
||||
&.neverSaved {
|
||||
translate:-100%;
|
||||
opacity: 0;
|
||||
background-color :#333;
|
||||
cursor:auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
require('./sharePage.less');
|
||||
const React = require('react');
|
||||
const { useState, useEffect, useCallback } = React;
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
import './sharePage.less';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import Headtags from '../../../../vitreum/headtags.js';
|
||||
const Meta = Headtags.Meta;
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
import Nav from '@navbar/nav.jsx';
|
||||
import Navbar from '@navbar/navbar.jsx';
|
||||
import MetadataNav from '@navbar/metadata.navitem.jsx';
|
||||
import PrintNavItem from '@navbar/print.navitem.jsx';
|
||||
import RecentNavItems from '@navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
import Account from '@navbar/account.navitem.jsx';
|
||||
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||
|
||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
|
||||
|
||||
const SharePage = (props)=>{
|
||||
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
|
||||
|
||||
const [state, setState] = useState({
|
||||
themeBundle : {},
|
||||
currentBrewRendererPageNum : 1,
|
||||
});
|
||||
const [themeBundle, setThemeBundle] = useState({});
|
||||
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||
|
||||
const handleBrewRendererPageChange = useCallback((pageNumber)=>{
|
||||
setState((prevState)=>({
|
||||
currentBrewRendererPageNum : pageNumber,
|
||||
...prevState }));
|
||||
setCurrentBrewRendererPageNum(pageNumber);
|
||||
}, []);
|
||||
|
||||
const handleControlKeys = (e)=>{
|
||||
@@ -40,11 +37,7 @@ const SharePage = (props)=>{
|
||||
|
||||
useEffect(()=>{
|
||||
document.addEventListener('keydown', handleControlKeys);
|
||||
fetchThemeBundle(
|
||||
{ setState },
|
||||
brew.renderer,
|
||||
brew.theme
|
||||
);
|
||||
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme);
|
||||
|
||||
const eventSource = new EventSource('/stream');
|
||||
|
||||
@@ -111,6 +104,19 @@ const SharePage = (props)=>{
|
||||
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${processShareId()}`}>
|
||||
clone to new
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
color='blue'
|
||||
icon='fas fa-link'
|
||||
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${processShareId()}`);}}>
|
||||
copy url
|
||||
</Nav.item>
|
||||
{currentBrewRendererPageNum > 1 &&
|
||||
<Nav.item
|
||||
color='blue'
|
||||
icon='fas fa-hashtag'
|
||||
onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${processShareId()}#p${currentBrewRendererPageNum}`);}}>
|
||||
copy url (page {currentBrewRendererPageNum})
|
||||
</Nav.item>}
|
||||
</Nav.dropdown>
|
||||
</>
|
||||
)}
|
||||
@@ -126,9 +132,9 @@ const SharePage = (props)=>{
|
||||
lang={brew.lang}
|
||||
renderer={brew.renderer}
|
||||
theme={brew.theme}
|
||||
themeBundle={state.themeBundle}
|
||||
themeBundle={themeBundle}
|
||||
onPageChange={handleBrewRendererPageChange}
|
||||
currentBrewRendererPageNum={state.currentBrewRendererPageNum}
|
||||
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||
allowPrint={true}
|
||||
/>
|
||||
</div>
|
||||
@@ -136,4 +142,4 @@ const SharePage = (props)=>{
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = SharePage;
|
||||
export default SharePage;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
const React = require('react');
|
||||
const { useState } = React;
|
||||
const _ = require('lodash');
|
||||
import React, { useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||
import ListPage from '../basePages/listPage/listPage.jsx';
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||
const VaultNavitem = require('../../navbar/vault.navitem.jsx');
|
||||
import Nav from '@navbar/nav.jsx';
|
||||
import Navbar from '@navbar/navbar.jsx';
|
||||
import RecentNavItems from '@navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
import Account from '@navbar/account.navitem.jsx';
|
||||
import NewBrew from '@navbar/newbrew.navitem.jsx';
|
||||
import HelpNavItem from '@navbar/help.navitem.jsx';
|
||||
import ErrorNavItem from '@navbar/error-navitem.jsx';
|
||||
import VaultNavitem from '@navbar/vault.navitem.jsx';
|
||||
|
||||
const UserPage = (props)=>{
|
||||
props = {
|
||||
@@ -39,10 +39,14 @@ const UserPage = (props)=>{
|
||||
}] : [])
|
||||
];
|
||||
|
||||
const clearError = ()=>{
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const navItems = (
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
{error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
|
||||
{error && (<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>)}
|
||||
<NewBrew />
|
||||
<HelpNavItem />
|
||||
<VaultNavitem />
|
||||
@@ -57,4 +61,4 @@ const UserPage = (props)=>{
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = UserPage;
|
||||
export default UserPage;
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||
/*eslint max-params:["warn", { max: 10 }], */
|
||||
require('./vaultPage.less');
|
||||
import './vaultPage.less';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const React = require('react');
|
||||
const { useState, useEffect, useRef } = React;
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
||||
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
||||
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
||||
import Nav from '@navbar/nav.jsx';
|
||||
import Navbar from '@navbar/navbar.jsx';
|
||||
import RecentNavItems from '@navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
import Account from '@navbar/account.navitem.jsx';
|
||||
import NewBrew from '@navbar/newbrew.navitem.jsx';
|
||||
import HelpNavItem from '@navbar/help.navitem.jsx';
|
||||
import BrewItem from '../basePages/listPage/brewItem/brewItem.jsx';
|
||||
import SplitPane from '@components/splitPane/splitPane.jsx';
|
||||
import ErrorIndex from '../errorPage/errors/errorIndex.js';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
|
||||
@@ -101,7 +100,7 @@ const VaultPage = (props)=>{
|
||||
|
||||
const title = titleRef.current.value || '';
|
||||
const author = authorRef.current.value || '';
|
||||
const count = countRef.current.value || 10;
|
||||
const count = countRef.current.value || 20;
|
||||
const v3 = v3Ref.current.checked != false;
|
||||
const legacy = legacyRef.current.checked != false;
|
||||
const sortOption = sort || 'title';
|
||||
@@ -288,7 +287,8 @@ const VaultPage = (props)=>{
|
||||
const renderPaginationControls = ()=>{
|
||||
if(!totalBrews || totalBrews < 10) return null;
|
||||
|
||||
const countInt = parseInt(brewCollection.length || 20);
|
||||
|
||||
const countInt = parseInt(countRef.current.value || 20);
|
||||
const totalPages = Math.ceil(totalBrews / countInt);
|
||||
|
||||
let startPage, endPage;
|
||||
@@ -429,4 +429,4 @@ const VaultPage = (props)=>{
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = VaultPage;
|
||||
export default VaultPage;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
@import '@sharedStyles/core.less';
|
||||
|
||||
.vaultPage {
|
||||
height : 100%;
|
||||
overflow-y : hidden;
|
||||
background-color : #2C3E50;
|
||||
|
||||
*:not(input) { user-select : none; }
|
||||
|
||||
.form {
|
||||
background:white;
|
||||
}
|
||||
|
||||
:where(.content .dataGroup) {
|
||||
width : 100%;
|
||||
height : 100%;
|
||||
background : white;
|
||||
|
||||
&.form .brewLookup {
|
||||
position : relative;
|
||||
@@ -171,7 +175,6 @@
|
||||
max-height : 100%;
|
||||
padding : 70px 50px;
|
||||
overflow-y : scroll;
|
||||
background-color : #2C3E50;
|
||||
container-type : inline-size;
|
||||
|
||||
h3 { font-size : 25px; }
|
||||
|
||||
Reference in New Issue
Block a user