0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-16 23:22:40 +00:00

Merge branch 'master' into View-Modes

This commit is contained in:
Gazook89
2024-11-04 14:22:54 -06:00
33 changed files with 2086 additions and 1721 deletions

View File

@@ -76,6 +76,9 @@ jobs:
- run: - run:
name: Test - Routes name: Test - Routes
command: npm run test:route command: npm run test:route
- run:
name: Test - HTML sanitization
command: npm run test:safehtml
- run: - run:
name: Test - Coverage name: Test - Coverage
command: npm run test:coverage command: npm run test:coverage

View File

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

View File

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

View File

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

View File

@@ -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, 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');
@@ -16,8 +16,7 @@ const Frame = require('react-frame-component').default;
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
const { printCurrentBrew } = require('../../../shared/helpers.js'); const { printCurrentBrew } = require('../../../shared/helpers.js');
const DOMPurify = require('dompurify'); import { safeHTML } from './safeHTML.js';
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
@@ -29,6 +28,7 @@ const INITIAL_CONTENT = dedent`
<base target=_blank> <base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`; </head><body style='overflow: hidden'><div></div></body></html>`;
//v=====----------------------< Brew Page Component >---------------------=====v// //v=====----------------------< Brew Page Component >---------------------=====v//
const BrewPage = (props)=>{ const BrewPage = (props)=>{
props = { props = {
@@ -36,7 +36,7 @@ const BrewPage = (props)=>{
index : 0, index : 0,
...props ...props
}; };
const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig); const cleanText = safeHTML(props.contents);
return <div className={props.className} id={`p${props.index + 1}`} style={props.style}> return <div className={props.className} id={`p${props.index + 1}`} style={props.style}>
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} /> <div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
</div>; </div>;
@@ -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)=>{
@@ -83,6 +83,26 @@ const BrewRenderer = (props)=>{
rawPages = props.text.split(/^\\page$/gm); rawPages = props.text.split(/^\\page$/gm);
} }
const scrollToHash = (hash)=>{
if(!hash) return;
const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
let anchor = iframeDoc.querySelector(hash);
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)=>{
const { scrollTop, clientHeight, scrollHeight } = e.target; const { scrollTop, clientHeight, scrollHeight } = e.target;
const totalScrollableHeight = scrollHeight - clientHeight; const totalScrollableHeight = scrollHeight - clientHeight;
@@ -111,9 +131,9 @@ const BrewRenderer = (props)=>{
}; };
const renderStyle = ()=>{ const renderStyle = ()=>{
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>'; const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `${themeStyles} \n\n <style> ${cleanStyle} </style>` }} />; const cleanStyle = safeHTML(`${themeStyles} \n\n <style> ${props.style} </style>`);
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: cleanStyle }} />;
}; };
const renderPage = (pageText, index)=>{ const renderPage = (pageText, index)=>{
@@ -162,6 +182,8 @@ 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
renderPages(); //Make sure page is renderable before showing renderPages(); //Make sure page is renderable before showing
setState((prevState)=>({ setState((prevState)=>({
@@ -190,9 +212,12 @@ const BrewRenderer = (props)=>{
const styleObject = {}; const styleObject = {};
if(global.config.deployment) { 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='white' font-size='20'>${global.config.deployment}</text></svg>")`; 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.*/}

View File

@@ -50,6 +50,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;

View File

@@ -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_notification01-10-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!&nbsp;
<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>;
}; };

View File

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

View File

@@ -0,0 +1,46 @@
// Derived from the vue-html-secure package, customized for Homebrewery
let doc = null;
let div = null;
function safeHTML(htmlString) {
// If the Document interface doesn't exist, exit
if(typeof document == 'undefined') return null;
// If the test document and div don't exist, create them
if(!doc) doc = document.implementation.createHTMLDocument('');
if(!div) div = doc.createElement('div');
// Set the test div contents to the evaluation string
div.innerHTML = htmlString;
// Grab all nodes from the test div
const elements = div.querySelectorAll('*');
// Blacklisted tags
const blacklistTags = ['script', 'noscript', 'noembed'];
// Tests to remove attributes
const blacklistAttrs = [
(test)=>{return test.localName.indexOf('on') == 0;},
(test)=>{return test.localName.indexOf('type') == 0 && test.value.match(/submit/i);},
(test)=>{return test.value.replace(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g, '').toLowerCase().trim().indexOf('javascript:') == 0;}
];
elements.forEach((element)=>{
// Check each element for blacklisted type
if(blacklistTags.includes(element?.localName?.toLowerCase())) {
element.remove();
return;
}
// Check remaining elements for blacklisted attributes
for (const attribute of element.attributes){
if(blacklistAttrs.some((test)=>{return test(attribute);})) {
element.removeAttribute(attribute.localName);
break;
};
};
});
return div.innerHTML;
};
module.exports.safeHTML = safeHTML;

View File

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

View File

@@ -2,6 +2,7 @@
.editor { .editor {
position : relative; position : relative;
width : 100%; width : 100%;
container: editor / inline-size;
.codeEditor { .codeEditor {
height : 100%; height : 100%;

View File

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

View File

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

View File

@@ -25,12 +25,11 @@
.homebrew nav { .homebrew nav {
background-color : #333333; background-color : #333333;
.navContent { position : relative;
position : relative; z-index : 2;
z-index : 2; display : flex;
display : flex; justify-content : space-between;
justify-content : space-between;
}
.navSection { .navSection {
display : flex; display : flex;
align-items : center; align-items : center;

View File

@@ -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);
@@ -429,42 +429,40 @@ const EditPage = createClass({
<Meta name='robots' content='noindex, nofollow' /> <Meta name='robots' content='noindex, nofollow' />
{this.renderNavbar()} {this.renderNavbar()}
<div className='content'> {this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />} <SplitPane onDragFinish={this.handleSplitMove}>
<SplitPane onDragFinish={this.handleSplitMove}> <Editor
<Editor ref={this.editor}
ref={this.editor} brew={this.state.brew}
brew={this.state.brew} onTextChange={this.handleTextChange}
onTextChange={this.handleTextChange} onStyleChange={this.handleStyleChange}
onStyleChange={this.handleStyleChange} onMetaChange={this.handleMetaChange}
onMetaChange={this.handleMetaChange} reportError={this.errorReported}
reportError={this.errorReported} renderer={this.state.brew.renderer}
renderer={this.state.brew.renderer} userThemes={this.props.userThemes}
userThemes={this.props.userThemes} snippetBundle={this.state.themeBundle.snippets}
snippetBundle={this.state.themeBundle.snippets} updateBrew={this.updateBrew}
updateBrew={this.updateBrew} onCursorPageChange={this.handleEditorCursorPageChange}
onCursorPageChange={this.handleEditorCursorPageChange} onViewPageChange={this.handleEditorViewPageChange}
onViewPageChange={this.handleEditorViewPageChange} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorViewPageNum={this.state.currentEditorViewPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} />
/> <BrewRenderer
<BrewRenderer text={this.state.brew.text}
text={this.state.brew.text} style={this.state.brew.style}
style={this.state.brew.style} renderer={this.state.brew.renderer}
renderer={this.state.brew.renderer} theme={this.state.brew.theme}
theme={this.state.brew.theme} themeBundle={this.state.themeBundle}
themeBundle={this.state.themeBundle} errors={this.state.htmlErrors}
errors={this.state.htmlErrors} lang={this.state.brew.lang}
lang={this.state.brew.lang} onPageChange={this.handleBrewRendererPageChange}
onPageChange={this.handleBrewRendererPageChange} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorViewPageNum={this.state.currentEditorViewPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} allowPrint={true}
allowPrint={true} />
/> </SplitPane>
</SplitPane>
</div>
</div>; </div>;
} }
}); });

View File

@@ -100,35 +100,31 @@ const HomePage = createClass({
return <div className='homePage sitePage'> return <div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' /> <Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{this.renderNavbar()} {this.renderNavbar()}
<SplitPane onDragFinish={this.handleSplitMove}>
<div className='content'> <Editor
<SplitPane onDragFinish={this.handleSplitMove}> ref={this.editor}
<Editor brew={this.state.brew}
ref={this.editor} onTextChange={this.handleTextChange}
brew={this.state.brew} renderer={this.state.brew.renderer}
onTextChange={this.handleTextChange} showEditButtons={false}
renderer={this.state.brew.renderer} snippetBundle={this.state.themeBundle.snippets}
showEditButtons={false} onCursorPageChange={this.handleEditorCursorPageChange}
snippetBundle={this.state.themeBundle.snippets} onViewPageChange={this.handleEditorViewPageChange}
onCursorPageChange={this.handleEditorCursorPageChange} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
onViewPageChange={this.handleEditorViewPageChange} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentEditorViewPageNum={this.state.currentEditorViewPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} />
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} <BrewRenderer
/> text={this.state.brew.text}
<BrewRenderer style={this.state.brew.style}
text={this.state.brew.text} renderer={this.state.brew.renderer}
style={this.state.brew.style} onPageChange={this.handleBrewRendererPageChange}
renderer={this.state.brew.renderer} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
onPageChange={this.handleBrewRendererPageChange} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentEditorViewPageNum={this.state.currentEditorViewPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} themeBundle={this.state.themeBundle}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} />
themeBundle={this.state.themeBundle} </SplitPane>
/>
</SplitPane>
</div>
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}> <div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
Save current <i className='fas fa-save' /> Save current <i className='fas fa-save' />
</div> </div>

View File

@@ -91,13 +91,6 @@ If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](
\page \page
## Markdown+ ## Markdown+
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML. The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.

View File

@@ -223,39 +223,37 @@ const NewPage = createClass({
render : function(){ render : function(){
return <div className='newPage sitePage'> return <div className='newPage sitePage'>
{this.renderNavbar()} {this.renderNavbar()}
<div className='content'> <SplitPane onDragFinish={this.handleSplitMove}>
<SplitPane onDragFinish={this.handleSplitMove}> <Editor
<Editor ref={this.editor}
ref={this.editor} brew={this.state.brew}
brew={this.state.brew} onTextChange={this.handleTextChange}
onTextChange={this.handleTextChange} onStyleChange={this.handleStyleChange}
onStyleChange={this.handleStyleChange} onMetaChange={this.handleMetaChange}
onMetaChange={this.handleMetaChange} renderer={this.state.brew.renderer}
renderer={this.state.brew.renderer} userThemes={this.props.userThemes}
userThemes={this.props.userThemes} snippetBundle={this.state.themeBundle.snippets}
snippetBundle={this.state.themeBundle.snippets} onCursorPageChange={this.handleEditorCursorPageChange}
onCursorPageChange={this.handleEditorCursorPageChange} onViewPageChange={this.handleEditorViewPageChange}
onViewPageChange={this.handleEditorViewPageChange} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorViewPageNum={this.state.currentEditorViewPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} />
/> <BrewRenderer
<BrewRenderer text={this.state.brew.text}
text={this.state.brew.text} style={this.state.brew.style}
style={this.state.brew.style} renderer={this.state.brew.renderer}
renderer={this.state.brew.renderer} theme={this.state.brew.theme}
theme={this.state.brew.theme} themeBundle={this.state.themeBundle}
themeBundle={this.state.themeBundle} errors={this.state.htmlErrors}
errors={this.state.htmlErrors} lang={this.state.brew.lang}
lang={this.state.brew.lang} onPageChange={this.handleBrewRendererPageChange}
onPageChange={this.handleBrewRendererPageChange} currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorViewPageNum={this.state.currentEditorViewPageNum} currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} allowPrint={true}
allowPrint={true} />
/> </SplitPane>
</SplitPane>
</div>
</div>; </div>;
} }
}); });

View File

@@ -1,5 +1,5 @@
.sharePage{ .sharePage{
.navContent .navSection.titleSection { nav .navSection.titleSection {
flex-grow: 1; flex-grow: 1;
justify-content: center; justify-content: center;
} }

View File

@@ -1,12 +1,11 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const { useState } = React;
const _ = require('lodash'); const _ = require('lodash');
const ListPage = require('../basePages/listPage/listPage.jsx'); const ListPage = require('../basePages/listPage/listPage.jsx');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx'); const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx'); const NewBrew = require('../../navbar/newbrew.navitem.jsx');
@@ -14,69 +13,48 @@ const HelpNavItem = require('../../navbar/help.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const VaultNavitem = require('../../navbar/vault.navitem.jsx'); const VaultNavitem = require('../../navbar/vault.navitem.jsx');
const UserPage = createClass({ const UserPage = (props)=>{
displayName : 'UserPage', props = {
getDefaultProps : function() { username : '',
return { brews : [],
username : '', query : '',
brews : [], ...props
query : '', };
error : null
};
},
getInitialState : function() {
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `` : `s`);
const brews = _.groupBy(this.props.brews, (brew)=>{ const [error, setError] = useState(null);
return (brew.published ? 'published' : 'private');
});
const brewCollection = [ const usernameWithS = props.username + (props.username.endsWith('s') ? `` : `s`);
{ const groupedBrews = _.groupBy(props.brews, (brew)=>brew.published ? 'published' : 'private');
title : `${usernameWithS} published brews`,
class : 'published',
brews : brews.published
}
];
if(this.props.username == global.account?.username){
brewCollection.push(
{
title : `${usernameWithS} unpublished brews`,
class : 'unpublished',
brews : brews.private
}
);
}
return { const brewCollection = [
brewCollection : brewCollection {
}; title : `${usernameWithS} published brews`,
}, class : 'published',
errorReported : function(error) { brews : groupedBrews.published || []
this.setState({ },
error ...(props.username === global.account?.username ? [{
}); title : `${usernameWithS} unpublished brews`,
}, class : 'unpublished',
brews : groupedBrews.private || []
}] : [])
];
navItems : function() { const navItems = (
return <Navbar> <Navbar>
<Nav.section> <Nav.section>
{this.state.error ? {error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
null
}
<NewBrew /> <NewBrew />
<HelpNavItem /> <HelpNavItem />
<VaultNavitem/> <VaultNavitem />
<RecentNavItem /> <RecentNavItem />
<Account /> <Account />
</Nav.section> </Nav.section>
</Navbar>; </Navbar>
}, );
render : function(){ return (
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>; <ListPage brewCollection={brewCollection} navItems={navItems} query={props.query} reportError={(err)=>setError(err)} />
} );
}); };
module.exports = UserPage; module.exports = UserPage;

View File

@@ -415,16 +415,13 @@ const VaultPage = (props)=>{
<link href='/themes/V3/Blank/style.css' rel='stylesheet' /> <link href='/themes/V3/Blank/style.css' rel='stylesheet' />
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' /> <link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
{renderNavItems()} {renderNavItems()}
<div className='content'> <SplitPane showDividerButtons={false}>
<SplitPane showDividerButtons={false}> <div className='form dataGroup'>{renderForm()}</div>
<div className='form dataGroup'>{renderForm()}</div> <div className='resultsContainer dataGroup'>
{renderSortBar()}
<div className='resultsContainer dataGroup'> {renderFoundBrews()}
{renderSortBar()} </div>
{renderFoundBrews()} </SplitPane>
</div>
</SplitPane>
</div>
</div> </div>
); );
}; };

View File

@@ -5,373 +5,369 @@
*:not(input) { user-select : none; } *:not(input) { user-select : none; }
.content { .dataGroup {
width : 100%;
height : 100%; height : 100%;
background : #2C3E50; background : white;
.dataGroup { &.form .brewLookup {
width : 100%; position : relative;
height : 100%; padding : 50px clamp(20px, 4vw, 50px);
background : white;
&.form .brewLookup { small {
position : relative; font-size : 10pt;
padding : 50px clamp(20px, 4vw, 50px); color : #555555;
small {
font-size : 10pt;
color : #555555;
a { color : #333333; } a { color : #333333; }
} }
code { code {
padding-inline : 5px; padding-inline : 5px;
font-family : monospace; font-family : monospace;
background : lightgrey; background : lightgrey;
border-radius : 5px; border-radius : 5px;
} }
h1, h2, h3, h4 { h1, h2, h3, h4 {
font-family : 'CodeBold'; font-family : 'CodeBold';
letter-spacing : 2px; letter-spacing : 2px;
} }
legend { legend {
h3 { h3 {
margin-block : 30px 20px; margin-block : 30px 20px;
font-size : 20px; font-size : 20px;
text-align : center;
border-bottom : 2px solid;
}
ul {
padding-inline : 30px 10px;
li {
margin-block : 5px;
line-height : calc(1em + 5px);
list-style : disc;
}
}
}
&::after {
position : absolute;
top : 0;
right : 0;
left : 0;
display : block;
padding : 10px;
font-weight : 900;
color : white;
white-space : pre-wrap;
content : 'Error:\A At least one renderer should be enabled to make a search';
background : rgb(255, 60, 60);
opacity : 0;
transition : opacity 0.5s;
}
&:not(:has(input[type='checkbox']:checked))::after { opacity : 1; }
.formTitle {
margin : 20px 0;
font-size : 30px;
color : black;
text-align : center; text-align : center;
border-bottom : 2px solid; border-bottom : 2px solid;
} }
ul {
.formContents { padding-inline : 30px 10px;
position : relative; li {
display : flex; margin-block : 5px;
flex-direction : column; line-height : calc(1em + 5px);
list-style : disc;
label {
display : flex;
align-items : center;
margin : 10px 0;
} }
select { margin : 0 10px; } }
input {
margin : 0 10px;
&:invalid { background : rgb(255, 188, 181); }
&[type='checkbox'] {
position : relative;
display : inline-block;
width : 50px;
height : 30px;
font-family : 'WalterTurncoat';
font-size : 20px;
font-weight : 800;
color : white;
letter-spacing : 2px;
appearance : none;
background : red;
isolation : isolate;
border-radius : 5px;
&::before,&::after {
position : absolute;
inset : 0;
z-index : 5;
padding-top : 2px;
text-align : center;
}
&::before {
display : block;
content : 'No';
}
&::after {
display : none;
content : 'Yes';
}
&:checked {
background : green;
&::before { display : none; }
&::after { display : block; }
}
}
}
#searchButton {
position : absolute;
right : 20px;
bottom : 0;
i {
margin-left : 10px;
animation-duration : 1000s;
}
}
}
} }
&.resultsContainer { &::after {
position : absolute;
top : 0;
right : 0;
left : 0;
display : block;
padding : 10px;
font-weight : 900;
color : white;
white-space : pre-wrap;
content : 'Error:\A At least one renderer should be enabled to make a search';
background : rgb(255, 60, 60);
opacity : 0;
transition : opacity 0.5s;
}
&:not(:has(input[type='checkbox']:checked))::after { opacity : 1; }
.formTitle {
margin : 20px 0;
font-size : 30px;
color : black;
text-align : center;
border-bottom : 2px solid;
}
.formContents {
position : relative;
display : flex; display : flex;
flex-direction : column; flex-direction : column;
height : 100%;
overflow-y : auto; label {
font-family : 'BookInsanityRemake'; display : flex;
font-size : 0.34cm; align-items : center;
margin : 10px 0;
h3 {
font-family : 'Open Sans';
font-weight : 900;
color : white;
} }
select { margin : 0 10px; }
.sort-container { input {
display : flex; margin : 0 10px;
flex-wrap : wrap;
column-gap : 15px; &:invalid { background : rgb(255, 188, 181); }
justify-content : center;
height : 30px;
color : white;
background-color : #555555;
border-top : 1px solid #666666;
border-bottom : 1px solid #666666;
.sort-option { &[type='checkbox'] {
display : flex; position : relative;
align-items : center; display : inline-block;
padding : 0 8px; width : 50px;
height : 30px;
&:hover { background-color : #444444; } font-family : 'WalterTurncoat';
font-size : 20px;
&.active { font-weight : 800;
background-color : #333333; color : white;
letter-spacing : 2px;
button { appearance : none;
font-weight : 800; background : red;
color : white; isolation : isolate;
border-radius : 5px;
& + .sortDir { padding-left : 5px; }
} &::before,&::after {
position : absolute;
inset : 0;
z-index : 5;
padding-top : 2px;
text-align : center;
} }
button { &::before {
padding : 0; display : block;
font-size : 11px; content : 'No';
font-weight : normal;
color : #CCCCCC;
text-transform : uppercase;
background-color : transparent;
&:hover { background : none; }
}
}
}
.foundBrews {
position : relative;
width : 100%;
height : 100%;
max-height : 100%;
padding : 50px 50px 70px 50px;
overflow-y : scroll;
background-color : #2C3E50;
h3 { font-size : 25px; }
&.noBrews {
display : grid;
place-items : center;
color : white;
}
&.searching {
display : grid;
place-items : center;
color : white;
h3 { position : relative; }
h3.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : calc(100% + 5px) -50%;
animation : trailingDots 2s ease infinite;
}
}
.totalBrews {
position : fixed;
right : 0;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
font-family : 'Open Sans';
font-size : 11px;
font-weight : 800;
color : white;
background-color : #333333;
.searchAnim {
position : relative;
display : inline-block;
width : 3ch;
height : 1em;
} }
.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : -50% -50%;
animation : trailingDots 2s ease infinite;
}
}
.brewItem {
width : 47%;
margin-right : 40px;
color : black;
isolation : isolate;
&::after { &::after {
position : absolute; display : none;
inset : 0; content : 'Yes';
z-index : -2;
display : block;
content : '';
background-image : url('/assets/parchmentBackground.jpg');
} }
&:nth-child(even of .brewItem) { margin-right : 0; } &:checked {
background : green;
h2 { &::before { display : none; }
font-family : 'MrEavesRemake'; &::after { display : block; }
font-size : 0.75cm;
font-weight : 800;
line-height : 0.988em;
color : var(--HB_Color_HeaderText);
} }
.info {
position : relative;
z-index : 2;
font-family : 'ScalySansRemake';
font-size : 1.2em;
>span {
margin-right : 12px;
line-height : 1.5em;
}
}
.links { z-index : 2; }
hr {
margin : 0px;
visibility : hidden;
}
.thumbnail { z-index : -1; }
} }
}
.paginationControls { #searchButton {
position : absolute; position : absolute;
left : 50%; right : 20px;
display : grid; bottom : 0;
grid-template-areas : 'previousPage currentPage nextPage';
grid-template-columns : 50px 1fr 50px;
place-items : center;
width : auto;
translate : -50%;
.pages {
display : flex;
grid-area : currentPage;
justify-content : space-evenly;
width : 100%;
height : 100%;
padding : 5px 8px;
text-align : center;
.pageNumber {
margin-inline : 1vw;
font-family : 'Open Sans';
font-weight : 900;
color : white;
text-underline-position : under;
text-wrap : nowrap;
cursor : pointer;
&.currentPage {
color : gold;
text-decoration : underline;
pointer-events : none;
}
&.firstPage { margin-right : -5px; }
&.lastPage { margin-left : -5px; }
}
}
button {
width : max-content;
&.previousPage { grid-area : previousPage; }
&.nextPage { grid-area : nextPage; }
}
i {
margin-left : 10px;
animation-duration : 1000s;
} }
} }
} }
} }
&.resultsContainer {
display : flex;
flex-direction : column;
height : 100%;
overflow-y : auto;
font-family : 'BookInsanityRemake';
font-size : 0.34cm;
h3 {
font-family : 'Open Sans';
font-weight : 900;
color : white;
}
.sort-container {
display : flex;
flex-wrap : wrap;
column-gap : 15px;
justify-content : center;
height : 30px;
color : white;
background-color : #555555;
border-top : 1px solid #666666;
border-bottom : 1px solid #666666;
.sort-option {
display : flex;
align-items : center;
padding : 0 8px;
&:hover { background-color : #444444; }
&.active {
background-color : #333333;
button {
font-weight : 800;
color : white;
& + .sortDir { padding-left : 5px; }
}
}
button {
padding : 0;
font-size : 11px;
font-weight : normal;
color : #CCCCCC;
text-transform : uppercase;
background-color : transparent;
&:hover { background : none; }
}
}
}
.foundBrews {
position : relative;
width : 100%;
height : 100%;
max-height : 100%;
padding : 50px 50px 70px 50px;
overflow-y : scroll;
background-color : #2C3E50;
h3 { font-size : 25px; }
&.noBrews {
display : grid;
place-items : center;
color : white;
}
&.searching {
display : grid;
place-items : center;
color : white;
h3 { position : relative; }
h3.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : calc(100% + 5px) -50%;
animation : trailingDots 2s ease infinite;
}
}
.totalBrews {
position : fixed;
right : 0;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
font-family : 'Open Sans';
font-size : 11px;
font-weight : 800;
color : white;
background-color : #333333;
.searchAnim {
position : relative;
display : inline-block;
width : 3ch;
height : 1em;
}
.searchAnim::after {
position : absolute;
top : 50%;
right : 0;
width : max-content;
height : 1em;
content : '';
translate : -50% -50%;
animation : trailingDots 2s ease infinite;
}
}
.brewItem {
width : 47%;
margin-right : 40px;
color : black;
isolation : isolate;
&::after {
position : absolute;
inset : 0;
z-index : -2;
display : block;
content : '';
background-image : url('/assets/parchmentBackground.jpg');
}
&:nth-child(even of .brewItem) { margin-right : 0; }
h2 {
font-family : 'MrEavesRemake';
font-size : 0.75cm;
font-weight : 800;
line-height : 0.988em;
color : var(--HB_Color_HeaderText);
}
.info {
position : relative;
z-index : 2;
font-family : 'ScalySansRemake';
font-size : 1.2em;
>span {
margin-right : 12px;
line-height : 1.5em;
}
}
.links { z-index : 2; }
hr {
margin : 0px;
visibility : hidden;
}
.thumbnail { z-index : -1; }
}
.paginationControls {
position : absolute;
left : 50%;
display : grid;
grid-template-areas : 'previousPage currentPage nextPage';
grid-template-columns : 50px 1fr 50px;
place-items : center;
width : auto;
translate : -50%;
.pages {
display : flex;
grid-area : currentPage;
justify-content : space-evenly;
width : 100%;
height : 100%;
padding : 5px 8px;
text-align : center;
.pageNumber {
margin-inline : 1vw;
font-family : 'Open Sans';
font-weight : 900;
color : white;
text-underline-position : under;
text-wrap : nowrap;
cursor : pointer;
&.currentPage {
color : gold;
text-decoration : underline;
pointer-events : none;
}
&.firstPage { margin-right : -5px; }
&.lastPage { margin-left : -5px; }
}
}
button {
width : max-content;
&.previousPage { grid-area : previousPage; }
&.nextPage { grid-area : nextPage; }
}
}
}
}
} }
} }
@keyframes trailingDots { @keyframes trailingDots {
@@ -388,7 +384,7 @@
// media query for when the page is smaller than 1079 px in width // media query for when the page is smaller than 1079 px in width
@media screen and (max-width : 1079px) { @media screen and (max-width : 1079px) {
.vaultPage .content { .vaultPage {
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; } .dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }

View File

@@ -0,0 +1,19 @@
import * as IDB from 'idb-keyval/dist/index.js';
export function initCustomStore(db, store){
const createCustomStore = async ()=>IDB.createStore(db, store);
return {
entries : async ()=>IDB.entries(await createCustomStore()),
keys : async ()=>IDB.keys(await createCustomStore()),
values : async ()=>IDB.values(await createCustomStore()),
clear : async ()=>IDB.clear(await createCustomStore),
get : async (key)=>IDB.get(key, await createCustomStore()),
getMany : async (keys)=>IDB.getMany(keys, await createCustomStore()),
set : async (key, value)=>IDB.set(key, value, await createCustomStore()),
setMany : async (entries)=>IDB.setMany(entries, await createCustomStore()),
update : async (key, updateFn)=>IDB.update(key, updateFn, await createCustomStore()),
del : async (key)=>IDB.del(key, await createCustomStore()),
delMany : async (keys)=>IDB.delMany(keys, await createCustomStore())
};
};

View File

@@ -1,4 +1,4 @@
import * as IDB from 'idb-keyval/dist/index.js'; import { initCustomStore } from './customIDBStore.js';
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY'; export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
export const HISTORY_SLOTS = 5; export const HISTORY_SLOTS = 5;
@@ -21,13 +21,15 @@ const HISTORY_SAVE_DELAYS = {
// '5' : 5 // '5' : 5
// }; // };
const HB_DB = 'HOMEBREWERY-DB';
const HB_STORE = 'HISTORY';
const GARBAGE_COLLECT_DELAY = 28 * 24 * 60; const GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
// const GARBAGE_COLLECT_DELAY = 10; // const GARBAGE_COLLECT_DELAY = 10;
const HB_DB = 'HOMEBREWERY-DB';
const HB_STORE = 'HISTORY';
const IDB = initCustomStore(HB_DB, HB_STORE);
function getKeyBySlot(brew, slot){ function getKeyBySlot(brew, slot){
// Return a string representing the key for this brew and history slot // Return a string representing the key for this brew and history slot
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`; return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
@@ -53,11 +55,6 @@ function parseBrewForStorage(brew, slot = 0) {
return [key, archiveBrew]; return [key, archiveBrew];
} }
// Create a custom IDB store
async function createHBStore(){
return await IDB.createStore(HB_DB, HB_STORE);
}
export async function loadHistory(brew){ export async function loadHistory(brew){
const DEFAULT_HISTORY_ITEM = { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true }; const DEFAULT_HISTORY_ITEM = { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
@@ -69,7 +66,7 @@ export async function loadHistory(brew){
}; };
// Load all keys from IDB at once // Load all keys from IDB at once
const dataArray = await IDB.getMany(historyKeys, await createHBStore()); const dataArray = await IDB.getMany(historyKeys);
return dataArray.map((data)=>{ return data ?? DEFAULT_HISTORY_ITEM; }); return dataArray.map((data)=>{ return data ?? DEFAULT_HISTORY_ITEM; });
} }
@@ -97,7 +94,7 @@ export async function updateHistory(brew) {
// Update the most recent brew // Update the most recent brew
historyUpdate.push(parseBrewForStorage(brew, 1)); historyUpdate.push(parseBrewForStorage(brew, 1));
await IDB.setMany(historyUpdate, await createHBStore()); await IDB.setMany(historyUpdate);
// Break out of data checks because we found an expired value // Break out of data checks because we found an expired value
break; break;
@@ -106,14 +103,17 @@ export async function updateHistory(brew) {
}; };
export async function versionHistoryGarbageCollection(){ export async function versionHistoryGarbageCollection(){
const entries = await IDB.entries();
const entries = await IDB.entries(await createHBStore()); const expiredKeys = [];
for (const [key, value] of entries){ for (const [key, value] of entries){
const expireAt = new Date(value.savedAt); const expireAt = new Date(value.savedAt);
expireAt.setMinutes(expireAt.getMinutes() + GARBAGE_COLLECT_DELAY); expireAt.setMinutes(expireAt.getMinutes() + GARBAGE_COLLECT_DELAY);
if(new Date() > expireAt){ if(new Date() > expireAt){
await IDB.del(key, await createHBStore()); expiredKeys.push(key);
}; };
}; };
if(expiredKeys.length > 0){
await IDB.delMany(expiredKeys);
}
}; };

1793
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace", "test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace", "test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
"test:route": "jest tests/routes/static-pages.test.js --verbose", "test:route": "jest tests/routes/static-pages.test.js --verbose",
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
"phb": "node --experimental-require-module scripts/phb.js", "phb": "node --experimental-require-module scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build", "prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run build", "postinstall": "npm run build",
@@ -86,10 +87,10 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.25.8", "@babel/core": "^7.26.0",
"@babel/plugin-transform-runtime": "^7.25.7", "@babel/plugin-transform-runtime": "^7.25.9",
"@babel/preset-env": "^7.25.8", "@babel/preset-env": "^7.26.0",
"@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 +116,28 @@
"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.3",
"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",
"jsdom-global": "^3.0.2",
"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"

View File

@@ -144,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 });
@@ -151,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);

View File

@@ -12,10 +12,8 @@ const Nav = {
displayName : 'Nav.base', displayName : 'Nav.base',
render : function(){ render : function(){
return <nav> return <nav>
<div className='navContent'>
{this.props.children} {this.props.children}
</div> </nav>;
</nav>;
} }
}), }),
logo : function(){ logo : function(){

View File

@@ -1,200 +1,110 @@
require('./splitPane.less'); require('./splitPane.less');
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const { useState, useEffect } = React;
const cx = require('classnames');
const SplitPane = createClass({ const storageKey = 'naturalcrit-pane-split';
displayName : 'SplitPane',
getDefaultProps : function() {
return {
storageKey : 'naturalcrit-pane-split',
onDragFinish : function(){}, //fires when dragging
showDividerButtons : true
};
},
getInitialState : function() { const SplitPane = (props)=>{
return { const {
currentDividerPos : null, onDragFinish = ()=>{},
windowWidth : 0, showDividerButtons = true
isDragging : false, } = props;
moveSource : false,
moveBrew : false,
showMoveArrows : true
};
},
pane1 : React.createRef(null), const [isDragging, setIsDragging] = useState(false);
pane2 : React.createRef(null), const [dividerPos, setDividerPos] = useState(null);
const [moveSource, setMoveSource] = useState(false);
const [moveBrew, setMoveBrew] = useState(false);
const [showMoveArrows, setShowMoveArrows] = useState(true);
const [liveScroll, setLiveScroll] = useState(false);
componentDidMount : function() { useEffect(()=>{
const dividerPos = window.localStorage.getItem(this.props.storageKey); const savedPos = window.localStorage.getItem(storageKey);
if(dividerPos){ setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
this.setState({ setLiveScroll(window.localStorage.getItem('liveScroll') === 'true');
currentDividerPos : this.limitPosition(dividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13)),
userSetDividerPos : dividerPos,
windowWidth : window.innerWidth
});
} else {
this.setState({
currentDividerPos : window.innerWidth / 2,
userSetDividerPos : window.innerWidth / 2
});
}
window.addEventListener('resize', this.handleWindowResize);
// This lives here instead of in the initial render because you cannot touch localStorage until the componant mounts. window.addEventListener('resize', handleResize);
const loadLiveScroll = window.localStorage.getItem('liveScroll') === 'true'; return ()=>window.removeEventListener('resize', handleResize);
this.setState({ liveScroll: loadLiveScroll }); }, []);
},
componentWillUnmount : function() { const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
window.removeEventListener('resize', this.handleWindowResize);
},
handleWindowResize : function() { //when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
// Allow divider to increase in size to last user-set position const handleResize = () =>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
// Limit current position to between 10% and 90% of visible space
const newLoc = this.limitPosition(this.state.userSetDividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13)); const handleUp =(e)=>{
this.setState({
currentDividerPos : newLoc,
windowWidth : window.innerWidth
});
},
limitPosition : function(x, min = 1, max = window.innerWidth - 13) {
const result = Math.round(Math.min(max, Math.max(min, x)));
return result;
},
handleUp : function(e){
e.preventDefault(); e.preventDefault();
if(this.state.isDragging){ if(isDragging) {
this.props.onDragFinish(this.state.currentDividerPos); onDragFinish(dividerPos);
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos); window.localStorage.setItem(storageKey, dividerPos);
} }
this.setState({ isDragging: false }); setIsDragging(false);
}, };
handleDown : function(e){ const handleDown = (e)=>{
e.preventDefault(); e.preventDefault();
this.setState({ isDragging: true }); setIsDragging(true);
//this.unFocus() };
},
handleMove : function(e){
if(!this.state.isDragging) return;
const handleMove = (e)=>{
if(!isDragging) return;
e.preventDefault(); e.preventDefault();
const newSize = this.limitPosition(e.pageX); setDividerPos(limitPosition(e.pageX));
this.setState({ };
currentDividerPos : newSize,
userSetDividerPos : newSize
});
},
liveScrollToggle : function() { const liveScrollToggle = ()=>{
window.localStorage.setItem('liveScroll', String(!this.state.liveScroll)); window.localStorage.setItem('liveScroll', String(!liveScroll));
this.setState({ liveScroll: !this.state.liveScroll }); setLiveScroll(!liveScroll);
}, };
/*
unFocus : function() {
if(document.selection){
document.selection.empty();
}else{
window.getSelection().removeAllRanges();
}
},
*/
setMoveArrows : function(newState) { const renderMoveArrows = (showMoveArrows &&
if(this.state.showMoveArrows != newState){ <>
this.setState({ <div className='arrow left'
showMoveArrows : newState onClick={()=>setMoveSource(!moveSource)} >
}); <i className='fas fa-arrow-left' />
}
},
renderMoveArrows : function(){
if(this.state.showMoveArrows) {
return <>
<div className='arrow left'
style={{ left: this.state.currentDividerPos-4 }}
onClick={()=>this.setState({ moveSource: !this.state.moveSource })} >
<i className='fas fa-arrow-left' />
</div>
<div className='arrow right'
style={{ left: this.state.currentDividerPos-4 }}
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
<i className='fas fa-arrow-right' />
</div>
<div id='scrollToggleDiv' className={this.state.liveScroll ? 'arrow lock' : 'arrow unlock'}
style={{ left: this.state.currentDividerPos-4 }}
onClick={this.liveScrollToggle} >
<i id='scrollToggle' className={this.state.liveScroll ? 'fas fa-lock' : 'fas fa-unlock'} />
</div>
</>;
}
},
renderDivider : function(){
return <>
{this.props.showDividerButtons && this.renderMoveArrows()}
<div className='divider' onPointerDown={this.handleDown} >
<div className='dots'>
<i className='fas fa-circle' />
<i className='fas fa-circle' />
<i className='fas fa-circle' />
</div>
</div> </div>
</>; <div className='arrow right'
}, onClick={()=>setMoveBrew(!moveBrew)} >
<i className='fas fa-arrow-right' />
</div>
<div id='scrollToggleDiv' className={liveScroll ? 'arrow lock' : 'arrow unlock'}
onClick={liveScrollToggle} >
<i id='scrollToggle' className={liveScroll ? 'fas fa-lock' : 'fas fa-unlock'} />
</div>
</>
);
render : function(){ const renderDivider = (
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}> <div className={`divider ${isDragging && 'dragging'}`} onPointerDown={handleDown}>
<Pane {showDividerButtons && renderMoveArrows}
width={this.state.currentDividerPos} <div className='dots'>
> <i className='fas fa-circle' />
{React.cloneElement(this.props.children[0], { <i className='fas fa-circle' />
...(this.props.showDividerButtons && { <i className='fas fa-circle' />
moveBrew : this.state.moveBrew, </div>
moveSource : this.state.moveSource, </div>
liveScroll : this.state.liveScroll, );
setMoveArrows : this.setMoveArrows,
}), return (
})} <div className='splitPane' onPointerMove={handleMove} onPointerUp={handleUp}>
<Pane width={dividerPos} moveBrew={moveBrew} moveSource={moveSource} liveScroll={liveScroll} setMoveArrows={setShowMoveArrows}>
{props.children[0]}
</Pane> </Pane>
{this.renderDivider()} {renderDivider}
<Pane isDragging={this.state.isDragging}>{this.props.children[1]}</Pane> <Pane isDragging={isDragging}>{props.children[1]}</Pane>
</div>; </div>
} );
}); };
const Pane = createClass({ const Pane = ({ width, children, isDragging, moveBrew, moveSource, liveScroll, setMoveArrows })=>{
displayName : 'Pane', const styles = width
getDefaultProps : function() { ? { flex: 'none', width: `${width}px` }
return { : { pointerEvents: isDragging ? 'none' : 'auto' }; //Disable mouse capture in the right pane; else dragging into the iframe drops the divider
width : null
};
},
render : function(){
let styles = {};
if(this.props.width){
styles = {
flex : 'none',
width : `${this.props.width}px`
};
} else {
styles = {
pointerEvents : this.props.isDragging ? 'none' : 'auto' //Disable mouse capture in the rightmost pane; dragging into the iframe drops the divider otherwise
};
}
return <div className={cx('pane', this.props.className)} style={styles}> return (
{this.props.children} <div className='pane' style={styles}>
</div>; {React.cloneElement(children, { moveBrew, moveSource, liveScroll, setMoveArrows })}
} </div>
}); );
};
module.exports = SplitPane; module.exports = SplitPane;

View File

@@ -1,69 +1,68 @@
.splitPane{ .splitPane {
position : relative; position : relative;
display : flex; display : flex;
flex-direction : row;
height : 100%; height : 100%;
outline : none; outline : none;
flex-direction : row; .pane {
.pane{ flex : 1;
overflow-x : hidden; overflow-x : hidden;
overflow-y : hidden; overflow-y : hidden;
flex : 1;
} }
.divider{ .divider {
touch-action : none; position : relative;
display : table; display : table;
height : 100%;
width : 15px; width : 15px;
cursor : ew-resize; height : 100%;
background-color : #bbb;
text-align : center; text-align : center;
.dots{ touch-action : none;
cursor : ew-resize;
background-color : #BBBBBB;
.dots {
display : table-cell; display : table-cell;
vertical-align : middle;
text-align : center; text-align : center;
i{ vertical-align : middle;
i {
display : block !important; display : block !important;
margin : 10px 0px; margin : 10px 0px;
font-size : 6px; font-size : 6px;
color : #666; color : #666666;
} }
} }
&:hover{ &:hover,&.dragging { background-color : #999999; }
background-color: #999;
}
} }
.arrow{ .arrow {
position : absolute; position : absolute;
left : 50%;
z-index : 999;
width : 25px; width : 25px;
height : 25px; height : 25px;
border : 2px solid #bbb;
border-radius : 15px;
text-align : center;
font-size : 1.2em; font-size : 1.2em;
text-align : center;
cursor : pointer; cursor : pointer;
background-color : #ddd; background-color : #DDDDDD;
z-index : 999; border : 2px solid #BBBBBB;
box-shadow : 0 4px 5px #0000007f; border-radius : 15px;
&.left{ box-shadow : 0 4px 5px #0000007F;
translate : -50%;
&.left {
.tooltipLeft('Jump to location in Editor'); .tooltipLeft('Jump to location in Editor');
top : 30px; top : 30px;
} }
&.right{ &.right {
.tooltipRight('Jump to location in Preview'); .tooltipRight('Jump to location in Preview');
top : 60px; top : 60px;
} }
&.lock{ &.lock {
.tooltipRight('De-sync Editor and Preview locations.'); .tooltipRight('De-sync Editor and Preview locations.');
top : 90px; top : 90px;
background: #666; background : #666666;
} }
&.unlock{ &.unlock {
.tooltipRight('Sync Editor and Preview locations'); .tooltipRight('Sync Editor and Preview locations');
top : 90px; top : 90px;
} }
&:hover{ &:hover { background-color : #666666; }
background-color: #666;
}
} }
} }

View File

@@ -0,0 +1,50 @@
require('jsdom-global')();
import { safeHTML } from '../../client/homebrew/brewRenderer/safeHTML';
test('Javascript via href', function() {
const source = `<a href="javascript:alert('This is a JavaScript injection via href attribute')">Click me</a>`;
const rendered = safeHTML(source);
expect(rendered).toBe('<a>Click me</a>');
});
test('Javascript via src', function() {
const source = `<img src="javascript:alert('This is a JavaScript injection via src attribute')">`;
const rendered = safeHTML(source);
expect(rendered).toBe('<img>');
});
test('Javascript via form submit action', function() {
const source = `<form action="javascript:alert('This is a JavaScript injection via action attribute')">\n<input type="submit" value="Submit">\n</form>`;
const rendered = safeHTML(source);
expect(rendered).toBe('<form>\n<input value=\"Submit\">\n</form>');
});
test('Javascript via inline event handler - onClick', function() {
const source = `<div style="background-color: red; color: white; width: 100px; height: 100px;" onclick="alert('This is a JavaScript injection via inline event handler')">\nClick me\n</div>`;
const rendered = safeHTML(source);
expect(rendered).toBe('<div style=\"background-color: red; color: white; width: 100px; height: 100px;\">\nClick me\n</div>');
});
test('Javascript via inline event handler - onMouseOver', function() {
const source = `<div onmouseover="alert('This is a JavaScript injection via inline event handler')">Hover over me</div>`;
const rendered = safeHTML(source);
expect(rendered).toBe('<div>Hover over me</div>');
});
test('Javascript via data attribute', function() {
const source = `<div data-code="javascript:alert('This is a JavaScript injection via data attribute')">Test</div>`;
const rendered = safeHTML(source);
expect(rendered).toBe('<div>Test</div>');
});
test('Javascript via event delegation', function() {
const source = `<div id="parent"><button id="child">Click me</button></div><script>document.getElementById('parent').addEventListener('click', function(event) {if (event.target.id === 'child') {console.log('This is JavaScript executed via event delegation');}});</script>`;
const rendered = safeHTML(source);
expect(rendered).toBe('<div id="parent"><button id="child">Click me</button></div>');
});

View File

@@ -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`,
},
]
}
] ]
}, },

View File

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