mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-07 07:42:39 +00:00
Merge branch 'master' into erase-unnecessary-divs
This commit is contained in:
76
changelog.md
76
changelog.md
@@ -81,9 +81,85 @@ pre {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Saturday 10/12/2024 - v3.16.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Added a new API endpoint `/metadata/:shareId` to fetch metadata about individual brews
|
||||||
|
|
||||||
|
Fixes issue [#2638](https://github.com/naturalcrit/homebrewery/issues/2638)
|
||||||
|
|
||||||
|
* [x] Added A3, A5, and Card page size snippets under {{openSans **:fas_paintbrush: STYLE TAB :fas_arrow_right: :fas_print: PRINT**}}
|
||||||
|
|
||||||
|
* [x] Adjust navbar styling for very long titles
|
||||||
|
|
||||||
|
Fixes issue [#2071](https://github.com/naturalcrit/homebrewery/issues/2071)
|
||||||
|
|
||||||
|
* [x] Added some sorting options to the {{openSans **VAULT** {{fas,fa-dungeon}}}} page
|
||||||
|
|
||||||
|
* [x] Fix `language` property not working in share page
|
||||||
|
|
||||||
|
Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776)
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] New {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBER :fas_arrow_right:**}}
|
||||||
|
{{openSans **:fas_xmark: SKIP PAGE NUMBER**}} and {{openSans **:fas_arrow_rotate_left: RESTART PAGE NUMBER**}} snippets for more control over automatic page numbering.
|
||||||
|
|
||||||
|
Fixes issue [#513](https://github.com/naturalcrit/homebrewery/issues/513)
|
||||||
|
|
||||||
|
* [x] New Table of Contents control options via {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_book: TABLE OF CONTENTS**}} submenus. By default, H1-H3 is included in the ToC generation, but the new options allow marking `{{blocks}}` to include or exclude specific or ranges of contained headers. Also, a global option to increase the default range of H1-H3 to H1-H4/5/6. After applying these markers, you must regenerate the Table of Contents to see the changes.
|
||||||
|
|
||||||
|
* [x] Added a ":fas_lock: SYNC VIEWS" button onto the divider bar. When locked, scrolling on either panel will sync the other panel to the same page.
|
||||||
|
|
||||||
|
Fixes issue [#241](https://github.com/naturalcrit/homebrewery/issues/241)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Added a :fas_glasses: HIDE button to the page navigation bar
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Automatic local backups of your files, in case of accidental data loss. Stores up to 5 snapshots of each brew edited in your browser, incrementing from a few minutes old to a maximum of several days. Restore a backup by clicking an entry in the new {{openSans **:fas_clock_rotate_left: HISTORY**}} button in the snippet bar.
|
||||||
|
|
||||||
|
Fixes issue [#3070](https://github.com/naturalcrit/homebrewery/issues/3070)
|
||||||
|
|
||||||
|
* [x] Fix issue with legacy brews breaking on Share page
|
||||||
|
|
||||||
|
Fixes issue [#3764](https://github.com/naturalcrit/homebrewery/issues/3764)
|
||||||
|
|
||||||
|
* [x] Fix print size when printing a zoomed document
|
||||||
|
|
||||||
|
Fixes issue [#3744](https://github.com/naturalcrit/homebrewery/issues/3744)
|
||||||
|
|
||||||
|
##### All
|
||||||
|
|
||||||
|
* [x] Background code cleanup, security fixes, dev tool improvements, dependency updates, prep for upcoming features, etc.
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 9/25/2024 - v3.15.1
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Background fixes to handle Google Drive issues
|
||||||
|
|
||||||
|
* [x] Remove duplicate error logging
|
||||||
|
|
||||||
|
##### calculuschild, 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix links in {{openSans **RECENT BREWS :fas_clock_rotate_left:**}} and user {{openSans **BREWS :fas_beer_mug_empty:**}} pointing to trashed Google Drive files after transferring from Google to Homebrewery storage
|
||||||
|
|
||||||
|
Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 9/04/2024 - v3.15.0
|
### Wednesday 9/04/2024 - v3.15.0
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const NotificationAdd = ()=>{
|
|||||||
<label className='field'>
|
<label className='field'>
|
||||||
Dismiss Key:
|
Dismiss Key:
|
||||||
<input className='fieldInput' type='text' ref={dismissKeyRef} required
|
<input className='fieldInput' type='text' ref={dismissKeyRef} required
|
||||||
placeholder='GOOGLEDRIVENOTIF'
|
placeholder='dismiss_notif_drive'
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ const NotificationDetail = ({ notification, onDelete })=>(
|
|||||||
<dt>Title</dt>
|
<dt>Title</dt>
|
||||||
<dd>{notification.title || 'No Title'}</dd>
|
<dd>{notification.title || 'No Title'}</dd>
|
||||||
|
|
||||||
<dt>Text</dt>
|
|
||||||
<dd>{notification.text || 'No Text'}</dd>
|
|
||||||
|
|
||||||
<dt>Created</dt>
|
<dt>Created</dt>
|
||||||
<dd>{Moment(notification.createdAt).format('LLLL')}</dd>
|
<dd>{Moment(notification.createdAt).format('LLLL')}</dd>
|
||||||
|
|
||||||
@@ -25,6 +22,9 @@ const NotificationDetail = ({ notification, onDelete })=>(
|
|||||||
|
|
||||||
<dt>Stop</dt>
|
<dt>Stop</dt>
|
||||||
<dd>{Moment(notification.stopAt).format('LLLL') || 'No End Time'}</dd>
|
<dd>{Moment(notification.stopAt).format('LLLL') || 'No End Time'}</dd>
|
||||||
|
|
||||||
|
<dt>Text</dt>
|
||||||
|
<dd>{notification.text || 'No Text'}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<button onClick={()=>onDelete(notification.dismissKey)}>DELETE</button>
|
<button onClick={()=>onDelete(notification.dismissKey)}>DELETE</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
// Dialog box, for popups and modal blocking messages
|
// Dialog box, for popups and modal blocking messages
|
||||||
const React = require('react');
|
import React from "react";
|
||||||
const { useRef, useEffect } = React;
|
const { useRef, useEffect } = React;
|
||||||
|
|
||||||
function Dialog({ dismissKey, closeText = 'Close', blocking = false, ...rest }) {
|
function Dialog({ dismisskeys, closeText = 'Close', blocking = false, ...rest }) {
|
||||||
const dialogRef = useRef(null);
|
const dialogRef = useRef(null);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(!dismissKey || !localStorage.getItem(dismissKey)) {
|
if (dismisskeys.length !== 0) {
|
||||||
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||||
}
|
}
|
||||||
}, []);
|
}, [dialogRef.current, dismisskeys]);
|
||||||
|
|
||||||
const dismiss = ()=>{
|
const dismiss = () => {
|
||||||
dismissKey && localStorage.setItem(dismissKey, true);
|
dismisskeys.forEach(key => {
|
||||||
|
if (key) {
|
||||||
|
localStorage.setItem(key, 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
dialogRef.current?.close();
|
dialogRef.current?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialog ref={dialogRef} onCancel={dismiss} {...rest}>
|
<dialog ref={dialogRef} onCancel={dismiss} {...rest}>
|
||||||
{rest.children}
|
{rest.children}
|
||||||
<button className='dismiss' onClick={dismiss}>
|
<button className='dismiss' onClick={dismiss}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./brewRenderer.less');
|
require('./brewRenderer.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useRef, useEffect, useCallback } = React;
|
const { useState, useRef, useCallback, useMemo } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
@@ -44,7 +44,7 @@ const BrewPage = (props)=>{
|
|||||||
|
|
||||||
|
|
||||||
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
||||||
const renderedPages = [];
|
let renderedPages = [];
|
||||||
let rawPages = [];
|
let rawPages = [];
|
||||||
|
|
||||||
const BrewRenderer = (props)=>{
|
const BrewRenderer = (props)=>{
|
||||||
@@ -64,7 +64,6 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
height : PAGE_HEIGHT,
|
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
visibility : 'hidden',
|
visibility : 'hidden',
|
||||||
zoom : 100
|
zoom : 100
|
||||||
@@ -78,15 +77,24 @@ const BrewRenderer = (props)=>{
|
|||||||
rawPages = props.text.split(/^\\page$/gm);
|
rawPages = props.text.split(/^\\page$/gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(()=>{ // Unmounting steps
|
const scrollToHash = (hash) => {
|
||||||
return ()=>{window.removeEventListener('resize', updateSize);};
|
if (!hash) return;
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateSize = ()=>{
|
const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
|
||||||
setState((prevState)=>({
|
let anchor = iframeDoc.querySelector(hash);
|
||||||
...prevState,
|
|
||||||
height : mainRef.current.parentNode.clientHeight,
|
if (anchor) {
|
||||||
}));
|
anchor.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
// Use MutationObserver to wait for the element if it's not immediately available
|
||||||
|
new MutationObserver((mutations, obs) => {
|
||||||
|
anchor = iframeDoc.querySelector(hash);
|
||||||
|
if (anchor) {
|
||||||
|
anchor.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
obs.disconnect();
|
||||||
|
}
|
||||||
|
}).observe(iframeDoc, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
||||||
@@ -162,9 +170,9 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
|
scrollToHash(window.location.hash);
|
||||||
|
|
||||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||||
updateSize();
|
|
||||||
window.addEventListener('resize', updateSize);
|
|
||||||
renderPages(); //Make sure page is renderable before showing
|
renderPages(); //Make sure page is renderable before showing
|
||||||
setState((prevState)=>({
|
setState((prevState)=>({
|
||||||
...prevState,
|
...prevState,
|
||||||
@@ -187,6 +195,15 @@ const BrewRenderer = (props)=>{
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styleObject = {};
|
||||||
|
|
||||||
|
if(global.config.deployment) {
|
||||||
|
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderedStyle = useMemo(()=> renderStyle(), [props.style, props.themeBundle]);
|
||||||
|
renderedPages = useMemo(() => renderPages(), [props.text]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*render dummy page while iFrame is mounting.*/}
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
@@ -212,19 +229,19 @@ const BrewRenderer = (props)=>{
|
|||||||
contentDidMount={frameDidMount}
|
contentDidMount={frameDidMount}
|
||||||
onClick={()=>{emitClick();}}
|
onClick={()=>{emitClick();}}
|
||||||
>
|
>
|
||||||
<div className={'brewRenderer'}
|
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
||||||
onScroll={updateCurrentPage}
|
onScroll={updateCurrentPage}
|
||||||
onKeyDown={handleControlKeys}
|
onKeyDown={handleControlKeys}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
style={{ height: state.height }}>
|
style={ styleObject }>
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderStyle()}
|
{renderedStyle}
|
||||||
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}>
|
<div className='pages' lang={`${props.lang || 'en'}`} style={{ zoom: `${state.zoom}%` }}>
|
||||||
{renderPages()}
|
{renderedPages}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
padding-top : 30px;
|
padding-top : 30px;
|
||||||
|
height : 100vh;
|
||||||
|
&.deployment {
|
||||||
|
background-color: darkred;
|
||||||
|
}
|
||||||
:where(.pages) {
|
:where(.pages) {
|
||||||
margin : 30px 0px;
|
margin : 30px 0px;
|
||||||
& > :where(.page) {
|
& > :where(.page) {
|
||||||
@@ -14,6 +18,9 @@
|
|||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
box-shadow : 1px 4px 14px #000000;
|
box-shadow : 1px 4px 14px #000000;
|
||||||
}
|
}
|
||||||
|
*[id] {
|
||||||
|
scroll-margin-top:100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width : 20px;
|
width : 20px;
|
||||||
|
|||||||
@@ -1,44 +1,62 @@
|
|||||||
require('./notificationPopup.less');
|
require('./notificationPopup.less');
|
||||||
const React = require('react');
|
import React, { useEffect, useState } from 'react';
|
||||||
const _ = require('lodash');
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
import Dialog from '../../../components/dialog.jsx';
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
|
|
||||||
const DISMISS_KEY = 'dismiss_notification04-09-24';
|
|
||||||
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||||
|
|
||||||
const NotificationPopup = ()=>{
|
const NotificationPopup = ()=>{
|
||||||
return <Dialog className='notificationPopup' dismissKey={DISMISS_KEY} closeText={DISMISS_BUTTON} >
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
const [dissmissKeyList, setDismissKeyList] = useState([]);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
getNotifications();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getNotifications = async ()=>{
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await request.get('/admin/notification/all');
|
||||||
|
pickActiveNotifications(res.body || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
setError(`Error looking up notifications: ${err?.response?.body?.message || err.message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pickActiveNotifications = (notifs)=>{
|
||||||
|
const now = new Date();
|
||||||
|
const filteredNotifications = notifs.filter((notification)=>{
|
||||||
|
const startDate = new Date(notification.startAt);
|
||||||
|
const stopDate = new Date(notification.stopAt);
|
||||||
|
const dismissed = localStorage.getItem(notification.dismissKey) ? true : false;
|
||||||
|
return now >= startDate && now <= stopDate && !dismissed;
|
||||||
|
});
|
||||||
|
setNotifications(filteredNotifications);
|
||||||
|
setDismissKeyList(filteredNotifications.map((notif)=>notif.dismissKey));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNotificationsList = ()=>{
|
||||||
|
if(error) return <div className='error'>{error}</div>;
|
||||||
|
|
||||||
|
return notifications.map((notification)=>(
|
||||||
|
<li key={notification.dismissKey} >
|
||||||
|
<em>{notification.title}</em><br />
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: notification.text }}></p>
|
||||||
|
</li>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
||||||
<div className='header'>
|
<div className='header'>
|
||||||
<i className='fas fa-info-circle info'></i>
|
<i className='fas fa-info-circle info'></i>
|
||||||
<h3>Notice</h3>
|
<h3>Notice</h3>
|
||||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
<li key='Vault'>
|
{renderNotificationsList()}
|
||||||
<em>Search brews with our new page!</em><br />
|
|
||||||
We have been working very hard in making this possible, now you can share your work and look at it in the new <a href='/vault'>Vault</a> page!
|
|
||||||
All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer.
|
|
||||||
|
|
||||||
More features will be coming.
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li key='googleDriveFolder'>
|
|
||||||
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
|
||||||
We have had several reports of users losing their brews, not realizing
|
|
||||||
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
|
||||||
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
|
||||||
We cannot help you recover files that you have deleted from your own
|
|
||||||
Google Drive.
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li key='faq'>
|
|
||||||
<em>Protect your work! </em> <br />
|
|
||||||
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
|
||||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
|
||||||
See the FAQ
|
|
||||||
</a> to learn how to avoid losing your work!
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,7 +55,10 @@
|
|||||||
margin-top : 1.4em;
|
margin-top : 1.4em;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
line-height : 1.4em;
|
line-height : 1.4em;
|
||||||
em { font-weight : 800; }
|
em {
|
||||||
|
text-transform:capitalize;
|
||||||
|
font-weight : 800;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
|
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
|
||||||
if(!window || isJumping)
|
if(!window || !this.isText() || isJumping)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Get current brewRenderer scroll position and calculate target position
|
// Get current brewRenderer scroll position and calculate target position
|
||||||
@@ -355,7 +355,7 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
|
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
|
||||||
if(!this.isText || isJumping)
|
if(!this.isText() || isJumping)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
.editor {
|
.editor {
|
||||||
position : relative;
|
position : relative;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
container: editor / inline-size;
|
||||||
|
|
||||||
.codeEditor {
|
.codeEditor {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
|
|||||||
@@ -150,18 +150,22 @@ const Snippetbar = createClass({
|
|||||||
|
|
||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
|
if(snippets.length === 0) return null;
|
||||||
|
|
||||||
return _.map(snippets, (snippetGroup)=>{
|
return <div className='snippets'>
|
||||||
return <SnippetGroup
|
{_.map(snippets, (snippetGroup)=>{
|
||||||
brew={this.props.brew}
|
return <SnippetGroup
|
||||||
groupName={snippetGroup.groupName}
|
brew={this.props.brew}
|
||||||
icon={snippetGroup.icon}
|
groupName={snippetGroup.groupName}
|
||||||
snippets={snippetGroup.snippets}
|
icon={snippetGroup.icon}
|
||||||
key={snippetGroup.groupName}
|
snippets={snippetGroup.snippets}
|
||||||
onSnippetClick={this.handleSnippetClick}
|
key={snippetGroup.groupName}
|
||||||
cursorPos={this.props.cursorPos}
|
onSnippetClick={this.handleSnippetClick}
|
||||||
/>;
|
cursorPos={this.props.cursorPos}
|
||||||
});
|
/>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
replaceContent : function(item){
|
replaceContent : function(item){
|
||||||
@@ -203,57 +207,58 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
let foldButtons;
|
const foldButtons = <>
|
||||||
if(this.props.view == 'text'){
|
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
|
||||||
foldButtons =
|
onClick={this.props.foldCode} >
|
||||||
<>
|
<i className='fas fa-compress-alt' />
|
||||||
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
</div>
|
||||||
onClick={this.props.foldCode} >
|
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
|
||||||
<i className='fas fa-compress-alt' />
|
onClick={this.props.unfoldCode} >
|
||||||
</div>
|
<i className='fas fa-expand-alt' />
|
||||||
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
</div>
|
||||||
onClick={this.props.unfoldCode} >
|
</>;
|
||||||
<i className='fas fa-expand-alt' />
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className='editors'>
|
return <div className='editors'>
|
||||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
<div className='historyTools'>
|
||||||
onClick={this.toggleHistoryMenu} >
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||||
<i className='fas fa-clock-rotate-left' />
|
onClick={this.toggleHistoryMenu} >
|
||||||
{ this.state.showHistory && this.renderHistoryItems() }
|
<i className='fas fa-clock-rotate-left' />
|
||||||
|
{ this.state.showHistory && this.renderHistoryItems() }
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
|
onClick={this.props.undo} >
|
||||||
|
<i className='fas fa-undo' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
|
||||||
|
onClick={this.props.redo} >
|
||||||
|
<i className='fas fa-redo' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
<div className='codeTools'>
|
||||||
onClick={this.props.undo} >
|
{foldButtons}
|
||||||
<i className='fas fa-undo' />
|
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
</div>
|
onClick={this.toggleThemeSelector} >
|
||||||
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
|
<i className='fas fa-palette' />
|
||||||
onClick={this.props.redo} >
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
<i className='fas fa-redo' />
|
</div>
|
||||||
</div>
|
|
||||||
<div className='divider'></div>
|
|
||||||
{foldButtons}
|
|
||||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
|
||||||
onClick={this.toggleThemeSelector} >
|
|
||||||
<i className='fas fa-palette' />
|
|
||||||
{this.state.themeSelector && this.renderThemeSelector()}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='divider'></div>
|
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className='tabs'>
|
||||||
onClick={()=>this.props.onViewChange('text')}>
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
<i className='fa fa-beer' />
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
</div>
|
<i className='fa fa-beer' />
|
||||||
<div className={cx('style', { selected: this.props.view === 'style' })}
|
</div>
|
||||||
onClick={()=>this.props.onViewChange('style')}>
|
<div className={cx('style', { selected: this.props.view === 'style' })}
|
||||||
<i className='fa fa-paint-brush' />
|
onClick={()=>this.props.onViewChange('style')}>
|
||||||
</div>
|
<i className='fa fa-paint-brush' />
|
||||||
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
</div>
|
||||||
onClick={()=>this.props.onViewChange('meta')}>
|
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
||||||
<i className='fas fa-info-circle' />
|
onClick={()=>this.props.onViewChange('meta')}>
|
||||||
|
<i className='fas fa-info-circle' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -291,8 +296,9 @@ const SnippetGroup = createClass({
|
|||||||
return _.map(snippets, (snippet)=>{
|
return _.map(snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
||||||
<i className={snippet.icon} />
|
<i className={snippet.icon} />
|
||||||
<span className='name'title={snippet.name}>{snippet.name}</span>
|
<span className={`name${snippet.disabled ? ' disabled' : ''}`} title={snippet.name}>{snippet.name}</span>
|
||||||
{snippet.experimental && <span className='beta'>beta</span>}
|
{snippet.experimental && <span className='beta'>beta</span>}
|
||||||
|
{snippet.disabled && <span className='beta' title='temporarily disabled due to large slowdown; under re-design'>disabled</span>}
|
||||||
{snippet.subsnippets && <>
|
{snippet.subsnippets && <>
|
||||||
<i className='fas fa-caret-right'></i>
|
<i className='fas fa-caret-right'></i>
|
||||||
<div className='dropdown side'>
|
<div className='dropdown side'>
|
||||||
|
|||||||
@@ -4,97 +4,114 @@
|
|||||||
.snippetBar {
|
.snippetBar {
|
||||||
@menuHeight : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
height : @menuHeight;
|
display : flex;
|
||||||
|
flex-wrap : wrap-reverse;
|
||||||
|
justify-content : space-between;
|
||||||
|
height : auto;
|
||||||
color : black;
|
color : black;
|
||||||
background-color : #DDDDDD;
|
background-color : #DDDDDD;
|
||||||
|
|
||||||
.editors {
|
.snippets {
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
right : 0px;
|
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : space-between;
|
justify-content : flex-start;
|
||||||
height : @menuHeight;
|
min-width : 327.58px;
|
||||||
& > div {
|
}
|
||||||
width : @menuHeight;
|
|
||||||
height : @menuHeight;
|
.editors {
|
||||||
line-height : @menuHeight;
|
display : flex;
|
||||||
text-align : center;
|
justify-content : flex-end;
|
||||||
cursor : pointer;
|
min-width : 225px;
|
||||||
&:hover,&.selected { background-color : #999999; }
|
|
||||||
&.text {
|
&:only-child { margin-left : auto; }
|
||||||
.tooltipLeft('Brew Editor');
|
|
||||||
}
|
>div {
|
||||||
&.style {
|
display : flex;
|
||||||
.tooltipLeft('Style Editor');
|
flex : 1;
|
||||||
}
|
justify-content : space-around;
|
||||||
&.meta {
|
|
||||||
.tooltipLeft('Properties');
|
&:first-child { border-left : none; }
|
||||||
}
|
|
||||||
&.undo {
|
& > div {
|
||||||
.tooltipLeft('Undo');
|
position : relative;
|
||||||
font-size : 0.75em;
|
width : @menuHeight;
|
||||||
color : grey;
|
height : @menuHeight;
|
||||||
&.active { color : inherit; }
|
line-height : @menuHeight;
|
||||||
}
|
text-align : center;
|
||||||
&.redo {
|
cursor : pointer;
|
||||||
.tooltipLeft('Redo');
|
&:hover,&.selected { background-color : #999999; }
|
||||||
font-size : 0.75em;
|
&.text {
|
||||||
color : grey;
|
.tooltipLeft('Brew Editor');
|
||||||
&.active { color : inherit; }
|
|
||||||
}
|
|
||||||
&.foldAll {
|
|
||||||
.tooltipLeft('Fold All');
|
|
||||||
font-size : 0.75em;
|
|
||||||
color : inherit;
|
|
||||||
}
|
|
||||||
&.unfoldAll {
|
|
||||||
.tooltipLeft('Unfold All');
|
|
||||||
font-size : 0.75em;
|
|
||||||
color : inherit;
|
|
||||||
}
|
|
||||||
&.history {
|
|
||||||
.tooltipLeft('History');
|
|
||||||
font-size : 0.75em;
|
|
||||||
color : grey;
|
|
||||||
position : relative;
|
|
||||||
&.active {
|
|
||||||
color : inherit;
|
|
||||||
}
|
}
|
||||||
&>.dropdown{
|
&.style {
|
||||||
right : -1px;
|
.tooltipLeft('Style Editor');
|
||||||
&>.snippet{
|
}
|
||||||
padding-right : 10px;
|
&.meta {
|
||||||
|
.tooltipLeft('Properties');
|
||||||
|
}
|
||||||
|
&.undo {
|
||||||
|
.tooltipLeft('Undo');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
&.active { color : inherit; }
|
||||||
|
}
|
||||||
|
&.redo {
|
||||||
|
.tooltipLeft('Redo');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
&.active { color : inherit; }
|
||||||
|
}
|
||||||
|
&.foldAll {
|
||||||
|
.tooltipLeft('Fold All');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
&.active { color : inherit; }
|
||||||
|
}
|
||||||
|
&.unfoldAll {
|
||||||
|
.tooltipLeft('Unfold All');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
&.active { color : inherit; }
|
||||||
|
}
|
||||||
|
&.history {
|
||||||
|
.tooltipLeft('History');
|
||||||
|
position : relative;
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
border : none;
|
||||||
|
&.active { color : inherit; }
|
||||||
|
& > .dropdown {
|
||||||
|
right : -1px;
|
||||||
|
& > .snippet { padding-right : 10px; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
&.editorTheme {
|
||||||
&.editorTheme {
|
.tooltipLeft('Editor Themes');
|
||||||
.tooltipLeft('Editor Themes');
|
font-size : 0.75em;
|
||||||
font-size : 0.75em;
|
color : black;
|
||||||
color : black;
|
&.active {
|
||||||
&.active {
|
position : relative;
|
||||||
position : relative;
|
background-color : #999999;
|
||||||
background-color : #999999;
|
}
|
||||||
|
}
|
||||||
|
&.divider {
|
||||||
|
width : 5px;
|
||||||
|
background : linear-gradient(currentColor, currentColor) no-repeat center/1px 100%;
|
||||||
|
&:hover { background-color : inherit; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.divider {
|
.themeSelector {
|
||||||
width : 5px;
|
position : absolute;
|
||||||
background : linear-gradient(currentColor, currentColor) no-repeat center/1px 100%;
|
top : 25px;
|
||||||
&:hover { background-color : inherit; }
|
right : 0;
|
||||||
|
z-index : 10;
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
justify-content : center;
|
||||||
|
width : 170px;
|
||||||
|
height : inherit;
|
||||||
|
background-color : inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.themeSelector {
|
|
||||||
position : absolute;
|
|
||||||
top : 25px;
|
|
||||||
right : 0;
|
|
||||||
z-index : 10;
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
justify-content : center;
|
|
||||||
width : 170px;
|
|
||||||
height : inherit;
|
|
||||||
background-color : inherit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.snippetBarButton {
|
.snippetBarButton {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
@@ -104,6 +121,7 @@
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
|
text-wrap : nowrap;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
&:hover, &.selected { background-color : #999999; }
|
&:hover, &.selected { background-color : #999999; }
|
||||||
i {
|
i {
|
||||||
@@ -120,7 +138,7 @@
|
|||||||
.tooltipLeft('Edit Brew Properties');
|
.tooltipLeft('Edit Brew Properties');
|
||||||
}
|
}
|
||||||
.snippetGroup {
|
.snippetGroup {
|
||||||
border-right : 1px solid currentColor;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
& > .dropdown { visibility : visible; }
|
& > .dropdown { visibility : visible; }
|
||||||
}
|
}
|
||||||
@@ -142,11 +160,11 @@
|
|||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
i {
|
i {
|
||||||
|
min-width : 25px;
|
||||||
height : 1.2em;
|
height : 1.2em;
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
min-width: 25px;
|
text-align : center;
|
||||||
text-align: center;
|
|
||||||
& ~ i {
|
& ~ i {
|
||||||
margin-right : 0;
|
margin-right : 0;
|
||||||
margin-left : 5px;
|
margin-left : 5px;
|
||||||
@@ -179,6 +197,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.name { margin-right : auto; }
|
.name { margin-right : auto; }
|
||||||
|
.disabled { text-decoration : line-through; }
|
||||||
.beta {
|
.beta {
|
||||||
align-self : center;
|
align-self : center;
|
||||||
padding : 4px 6px;
|
padding : 4px 6px;
|
||||||
@@ -204,4 +223,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@container editor (width < 553px) {
|
||||||
|
.snippetBar {
|
||||||
|
.editors {
|
||||||
|
flex : 1;
|
||||||
|
justify-content : space-between;
|
||||||
|
border-bottom : 1px solid;
|
||||||
|
}
|
||||||
|
.snippets {
|
||||||
|
flex : 1;
|
||||||
|
justify-content : space-evenly;
|
||||||
|
}
|
||||||
|
.editors > div.history > .dropdown { right : unset; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -228,8 +228,8 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await updateHistory(this.state.brew);
|
await updateHistory(this.state.brew).catch(console.error);
|
||||||
await versionHistoryGarbageCollection();
|
await versionHistoryGarbageCollection().catch(console.error);
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle}`,
|
**Brew Title:** ${props.brew.brewTitle}`,
|
||||||
|
|
||||||
|
// ####### Admin page error #######
|
||||||
|
'52': dedent`
|
||||||
|
## Access Denied
|
||||||
|
You need to provide correct administrator credentials to access this page.`,
|
||||||
|
|
||||||
'90' : dedent` An unexpected error occurred while looking for these brews.
|
'90' : dedent` An unexpected error occurred while looking for these brews.
|
||||||
Try again in a few minutes.`,
|
Try again in a few minutes.`,
|
||||||
|
|
||||||
|
|||||||
1411
package-lock.json
generated
1411
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.15.0",
|
"version": "3.16.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
"node": "^20.17.x"
|
"node": "^20.17.x"
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.25.7",
|
"@babel/core": "^7.25.9",
|
||||||
"@babel/plugin-transform-runtime": "^7.25.7",
|
"@babel/plugin-transform-runtime": "^7.25.9",
|
||||||
"@babel/preset-env": "^7.25.7",
|
"@babel/preset-env": "^7.25.9",
|
||||||
"@babel/preset-react": "^7.25.7",
|
"@babel/preset-react": "^7.25.9",
|
||||||
"@googleapis/drive": "^8.14.0",
|
"@googleapis/drive": "^8.14.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -115,27 +115,27 @@
|
|||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.7.1",
|
"mongoose": "^8.7.2",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.26.2",
|
"react-router-dom": "6.27.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.0",
|
"superagent": "^10.1.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"@stylistic/stylelint-plugin": "^3.1.1",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.13.0",
|
||||||
"eslint-plugin-jest": "^28.8.3",
|
"eslint-plugin-jest": "^28.8.3",
|
||||||
"eslint-plugin-react": "^7.37.1",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"globals": "^15.11.0",
|
"globals": "^15.11.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.9.0",
|
"stylelint": "^16.10.0",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^5.1.1",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^14.0.1",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0"
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ const HomebrewModel = require('./homebrew.model.js').model;
|
|||||||
const NotificationModel = require('./notifications.model.js').model;
|
const NotificationModel = require('./notifications.model.js').model;
|
||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const Moment = require('moment');
|
const Moment = require('moment');
|
||||||
//const render = require('vitreum/steps/render');
|
|
||||||
const templateFn = require('../client/template.js');
|
const templateFn = require('../client/template.js');
|
||||||
const zlib = require('zlib');
|
const zlib = require('zlib');
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ const mw = {
|
|||||||
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
return res.status(401).send('Access denied');
|
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -145,6 +144,7 @@ router.get('/admin/notification/all', async (req, res, next)=>{
|
|||||||
try {
|
try {
|
||||||
const notifications = await NotificationModel.getAll();
|
const notifications = await NotificationModel.getAll();
|
||||||
return res.json(notifications);
|
return res.json(notifications);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Error getting all notifications: ', error.message);
|
console.log('Error getting all notifications: ', error.message);
|
||||||
return res.status(500).json({ message: error.message });
|
return res.status(500).json({ message: error.message });
|
||||||
@@ -152,7 +152,6 @@ router.get('/admin/notification/all', async (req, res, next)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
|
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
|
||||||
console.table(req.body);
|
|
||||||
try {
|
try {
|
||||||
const notification = await NotificationModel.addNotification(req.body);
|
const notification = await NotificationModel.addNotification(req.body);
|
||||||
return res.status(201).json(notification);
|
return res.status(201).json(notification);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const app = express();
|
|||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
|
||||||
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = require('./homebrew.api.js');
|
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = require('./homebrew.api.js');
|
||||||
const GoogleActions = require('./googleActions.js');
|
const GoogleActions = require('./googleActions.js');
|
||||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||||
@@ -32,6 +31,8 @@ const sanitizeBrew = (brew, accessType)=>{
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
app.set('trust proxy', 1 /* number of proxies between user and server */)
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
app.use(require('./middleware/content-negotiation.js'));
|
app.use(require('./middleware/content-negotiation.js'));
|
||||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||||
@@ -257,6 +258,8 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
brews.forEach(brew => brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
if(ownAccount && req?.account?.googleId){
|
||||||
const auth = await GoogleActions.authCheck(req.account, res);
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||||
@@ -264,12 +267,12 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If stub matches file from Google, use Google metadata over stub metadata
|
||||||
if(googleBrews && googleBrews.length > 0) {
|
if(googleBrews && googleBrews.length > 0) {
|
||||||
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
||||||
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
||||||
if(match !== -1) {
|
if(match !== -1) {
|
||||||
brew.googleId = googleBrews[match].googleId;
|
brew.googleId = googleBrews[match].googleId;
|
||||||
brew.stubbed = true;
|
|
||||||
brew.pageCount = googleBrews[match].pageCount;
|
brew.pageCount = googleBrews[match].pageCount;
|
||||||
brew.renderer = googleBrews[match].renderer;
|
brew.renderer = googleBrews[match].renderer;
|
||||||
brew.version = googleBrews[match].version;
|
brew.version = googleBrews[match].version;
|
||||||
@@ -278,6 +281,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Remaining unstubbed google brews display current user as author
|
||||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||||
brews = _.concat(brews, googleBrews);
|
brews = _.concat(brews, googleBrews);
|
||||||
}
|
}
|
||||||
@@ -380,7 +384,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
|||||||
app.get('/account', asyncHandler(async (req, res, next)=>{
|
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||||
const data = {};
|
const data = {};
|
||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
if(!req.account) {
|
if(!req.account) {
|
||||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
||||||
const error = new Error('No valid account');
|
const error = new Error('No valid account');
|
||||||
@@ -394,22 +398,12 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
let googleCount = [];
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
try {
|
auth = await GoogleActions.authCheck(req.account, res, false)
|
||||||
auth = await GoogleActions.authCheck(req.account, res, false);
|
|
||||||
} catch (e) {
|
googleCount = await GoogleActions.listGoogleBrews(auth)
|
||||||
auth = undefined;
|
.catch((err)=>{
|
||||||
console.log('Google auth check failed!');
|
console.error(err);
|
||||||
console.log(e);
|
});
|
||||||
}
|
|
||||||
if(auth.credentials.access_token) {
|
|
||||||
try {
|
|
||||||
googleCount = await GoogleActions.listGoogleBrews(auth);
|
|
||||||
} catch (e) {
|
|
||||||
googleCount = undefined;
|
|
||||||
console.log('List Google files failed!');
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
@@ -423,7 +417,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
username : req.account.username,
|
username : req.account.username,
|
||||||
issued : req.account.issued,
|
issued : req.account.issued,
|
||||||
googleId : Boolean(req.account.googleId),
|
googleId : Boolean(req.account.googleId),
|
||||||
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
|
||||||
mongoCount : mongoCount,
|
mongoCount : mongoCount,
|
||||||
googleCount : googleCount?.length
|
googleCount : googleCount?.length
|
||||||
};
|
};
|
||||||
@@ -468,8 +462,8 @@ app.get('/vault', asyncHandler(async(req, res, next)=>{
|
|||||||
|
|
||||||
//Send rendered page
|
//Send rendered page
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
if(!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
||||||
|
|
||||||
const page = await renderPage(req, res);
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
res.send(page);
|
res.send(page);
|
||||||
@@ -481,7 +475,8 @@ const renderPage = async (req, res)=>{
|
|||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
publicUrl : config.get('publicUrl') ?? '',
|
publicUrl : config.get('publicUrl') ?? '',
|
||||||
environment : nodeEnv
|
environment : nodeEnv,
|
||||||
|
deployment : config.get('heroku_app_name') ?? ''
|
||||||
};
|
};
|
||||||
const props = {
|
const props = {
|
||||||
version : require('./../package.json').version,
|
version : require('./../package.json').version,
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ if(!config.get('service_account')){
|
|||||||
|
|
||||||
const defaultAuth = serviceAuth || config.get('google_api_key');
|
const defaultAuth = serviceAuth || config.get('google_api_key');
|
||||||
|
|
||||||
|
const retryConfig = {
|
||||||
|
retry: 3, // Number of retry attempts
|
||||||
|
retryDelay: 100, // Initial delay in milliseconds
|
||||||
|
retryDelayMultiplier: 2, // Multiplier for exponential backoff
|
||||||
|
maxRetryDelay: 32000, // Maximum delay in milliseconds
|
||||||
|
httpMethodsToRetry: ['PATCH'], // Only retry PATCH requests
|
||||||
|
statusCodesToRetry: [[429, 429]], // Only retry on 429 status code
|
||||||
|
};
|
||||||
|
|
||||||
const GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
authCheck : (account, res, updateTokens=true)=>{
|
authCheck : (account, res, updateTokens=true)=>{
|
||||||
@@ -112,9 +121,7 @@ const GoogleActions = {
|
|||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(`Error Listing Google Brews`);
|
console.log(`Error Listing Google Brews`);
|
||||||
console.error(err);
|
|
||||||
throw (err);
|
throw (err);
|
||||||
//TODO: Should break out here, but continues on for some reason.
|
|
||||||
});
|
});
|
||||||
fileList.push(...obj.data.files);
|
fileList.push(...obj.data.files);
|
||||||
NextPageToken = obj.data.nextPageToken;
|
NextPageToken = obj.data.nextPageToken;
|
||||||
@@ -147,7 +154,7 @@ const GoogleActions = {
|
|||||||
return brews;
|
return brews;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGoogleBrew : async (brew)=>{
|
updateGoogleBrew : async (brew, userIp)=>{
|
||||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
@@ -168,7 +175,11 @@ const GoogleActions = {
|
|||||||
media : {
|
media : {
|
||||||
mimeType : 'text/plain',
|
mimeType : 'text/plain',
|
||||||
body : brew.text
|
body : brew.text
|
||||||
}
|
},
|
||||||
|
headers: {
|
||||||
|
'X-Forwarded-For': userIp, // Set the X-Forwarded-For header
|
||||||
|
},
|
||||||
|
retryConfig
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error saving to google');
|
console.log('Error saving to google');
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ const api = {
|
|||||||
if(!brew.googleId) return;
|
if(!brew.googleId) return;
|
||||||
} else if(brew.googleId) {
|
} else if(brew.googleId) {
|
||||||
// If the google id exists and no other actions are being performed, update the google brew
|
// If the google id exists and no other actions are being performed, update the google brew
|
||||||
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew));
|
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
|
||||||
|
|
||||||
if(!updated) return;
|
if(!updated) return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,28 +154,6 @@ module.exports = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
name : 'Table of Contents Toggles',
|
|
||||||
icon : 'fas fa-book',
|
|
||||||
gen : `{{tocGlobalH4}}\n\n`,
|
|
||||||
subsnippets : [
|
|
||||||
{
|
|
||||||
name : 'Enable H1-H4 all pages',
|
|
||||||
icon : 'fas fa-dice-four',
|
|
||||||
gen : `{{tocGlobalH4}}\n\n`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Enable H1-H5 all pages',
|
|
||||||
icon : 'fas fa-dice-five',
|
|
||||||
gen : `{{tocGlobalH5}}\n\n`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Enable H1-H6 all pages',
|
|
||||||
icon : 'fas fa-dice-six',
|
|
||||||
gen : `{{tocGlobalH6}}\n\n`,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -214,6 +192,27 @@ module.exports = [
|
|||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
}\n\n`
|
}\n\n`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Table of Contents Toggles',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Enable H1-H4 all pages',
|
||||||
|
icon : 'fas fa-dice-four',
|
||||||
|
gen : `.page {\n\th4 {--TOC: include; }\n}\n\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Enable H1-H5 all pages',
|
||||||
|
icon : 'fas fa-dice-five',
|
||||||
|
gen : `.page {\n\th4, h5 {--TOC: include; }\n}\n\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Enable H1-H6 all pages',
|
||||||
|
icon : 'fas fa-dice-six',
|
||||||
|
gen : `.page {\n\th4, h5, h6 {--TOC: include; }\n}\n\n`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -812,17 +812,8 @@ h6,
|
|||||||
|
|
||||||
// Brew level default inclusion changes.
|
// Brew level default inclusion changes.
|
||||||
// These add Headers 'back' to inclusion.
|
// These add Headers 'back' to inclusion.
|
||||||
.pages:has(.tocGlobalH4) {
|
|
||||||
h4 {--TOC: include; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.pages:has(.tocGlobalH5) {
|
//NOTE: DO NOT USE :HAS WITH .PAGES!!! EXTREMELY SLOW TO RENDER ON LARGE DOCS!
|
||||||
h4, h5 {--TOC: include; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.pages:has(.tocGlobalH6) {
|
|
||||||
h4, h5, h6 {--TOC: include; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block level inclusion changes
|
// Block level inclusion changes
|
||||||
// These include either a single (include) or a range (depth)
|
// These include either a single (include) or a range (depth)
|
||||||
|
|||||||
Reference in New Issue
Block a user