0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-08 20:23:39 +00:00

Merge branch 'master' into experimentalNotificationDB

This commit is contained in:
Trevor Buckner
2024-10-09 11:12:23 -04:00
committed by GitHub
21 changed files with 927 additions and 1007 deletions

View File

@@ -55,9 +55,9 @@ const BrewRenderer = (props)=>{
theme : '5ePHB', theme : '5ePHB',
lang : '', lang : '',
errors : [], errors : [],
currentEditorCursorPageNum : 0, currentEditorCursorPageNum : 1,
currentEditorViewPageNum : 0, currentEditorViewPageNum : 1,
currentBrewRendererPageNum : 0, currentBrewRendererPageNum : 1,
themeBundle : {}, themeBundle : {},
onPageChange : ()=>{}, onPageChange : ()=>{},
...props ...props

View File

@@ -39,6 +39,7 @@
overflow-y : unset; overflow-y : unset;
.pages { .pages {
margin : 0px; margin : 0px;
zoom: 100% !important;
& > .page { box-shadow : unset; } & > .page { box-shadow : unset; }
} }
} }

View File

@@ -304,17 +304,14 @@ const MetadataEditor = createClass({
onChange={(e)=>this.handleRenderer('V3', e)} /> onChange={(e)=>this.handleRenderer('V3', e)} />
V3 V3
</label> </label>
<small><a href='/legacy' target='_blank' rel='noopener noreferrer'>Click here to see the demo page for the old Legacy renderer!</a></small>
<a href='/legacy' target='_blank' rel='noopener noreferrer'>
Click here to see the demo page for the old Legacy renderer!
</a>
</div> </div>
</div>; </div>;
}, },
render : function(){ render : function(){
return <div className='metadataEditor'> return <div className='metadataEditor'>
<h1 className='sectionHead'>Brew</h1> <h1>Properties Editor</h1>
<div className='field title'> <div className='field title'>
<label>title</label> <label>title</label>
@@ -362,9 +359,7 @@ const MetadataEditor = createClass({
{this.renderRenderOptions()} {this.renderRenderOptions()}
<hr/> <h2>Authors</h2>
<h1 className='sectionHead'>Authors</h1>
{this.renderAuthors()} {this.renderAuthors()}
@@ -375,15 +370,13 @@ const MetadataEditor = createClass({
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/> onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
<hr/> <h2>Privacy</h2>
<h1 className='sectionHead'>Privacy</h1>
<div className='field publish'> <div className='field publish'>
<label>publish</label> <label>publish</label>
<div className='value'> <div className='value'>
{this.renderPublish()} {this.renderPublish()}
<small>Published homebrews will be publicly viewable and searchable (eventually...)</small> <small>Published brews are searchable in <a href='/vault'>the Vault</a> and visible on your user page. Unpublished brews are not indexed in the Vault or visible on your user page, but can still be shared and indexed by search engines. You can unpublish a brew any time.</small>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,6 @@
@import 'naturalcrit/styles/colors.less'; @import 'naturalcrit/styles/colors.less';
.metadataEditor { .metadataEditor {
position : absolute; position : absolute;
z-index : 5; z-index : 5;
@@ -9,12 +10,19 @@
padding : 25px; padding : 25px;
overflow-y : auto; overflow-y : auto;
background-color : #999999; background-color : #999999;
font-size : 13px;
.sectionHead { h1 {
margin: 0 0 40px;
font-weight: bold;
text-transform: uppercase;
}
h2 {
margin : 20px 0; margin : 20px 0;
font-weight : 1000; font-weight : bold;
border-bottom: 2px solid gray;
&:first-of-type { margin-top : 0; } color: #555;
} }
& > div { margin-bottom : 10px; } & > div { margin-bottom : 10px; }
@@ -43,15 +51,21 @@
min-width : 200px; min-width : 200px;
& > label { & > label {
width : 80px; width : 80px;
font-size : 11px;
font-weight : 800; font-weight : 800;
line-height : 1.8em; line-height : 1.8em;
text-transform : uppercase; text-transform : uppercase;
font-size: .9em;
} }
& > .value { & > .value {
flex : 1 1 auto; flex : 1 1 auto;
width : 50px; width : 50px;
&:invalid { background : #FFB9B9; } &:invalid { background : #FFB9B9; }
small {
display : block;
font-size : 0.9em;
font-style : italic;
line-height : 1.4em;
}
} }
input[type='text'], textarea { input[type='text'], textarea {
border : 1px solid gray; border : 1px solid gray;
@@ -78,7 +92,6 @@
textarea.value { textarea.value {
height : auto; height : auto;
font-family : 'Open Sans', sans-serif; font-family : 'Open Sans', sans-serif;
font-size : 0.8em;
resize : none; resize : none;
} }
} }
@@ -87,12 +100,6 @@
z-index : 200; z-index : 200;
max-width : 150px; max-width : 150px;
} }
small {
display : inline-block;
font-size : 0.6em;
font-style : italic;
line-height : 1.4em;
}
} }
@@ -113,18 +120,13 @@
display : inline-flex; display : inline-flex;
align-items : center; align-items : center;
margin-right : 15px; margin-right : 15px;
font-size : 0.7em; font-size : 0.9em;
font-weight : 800; font-weight : 800;
white-space : nowrap; white-space : nowrap;
vertical-align : middle; vertical-align : middle;
cursor : pointer; cursor : pointer;
user-select : none; user-select : none;
} }
a {
display : inline-flex;
font-size : 0.7em;
font-weight : 800;
}
input { input {
margin : 3px; margin : 3px;
vertical-align : middle; vertical-align : middle;
@@ -149,12 +151,10 @@
} }
} }
.authors.field .value { .authors.field .value {
font-size : 0.8em;
line-height : 1.5em; line-height : 1.5em;
} }
.themes.field { .themes.field {
font-size : 13.33px;
.navDropdownContainer { .navDropdownContainer {
position : relative; position : relative;
z-index : 100; z-index : 100;
@@ -165,9 +165,9 @@
background-color : darkgray; background-color : darkgray;
} }
& > div:first-child { & > div:first-child {
padding : 6px 3px; padding : 3px 3px;
background-color : inherit; background-color : inherit;
border : 2px solid rgb(118,118,118); border : 1px solid gray;
i { float : right; } i { float : right; }
&:hover { &:hover {
color : white; color : white;
@@ -240,6 +240,7 @@
} }
} }
} }
.field .list { .field .list {
display : flex; display : flex;
flex : 1 0; flex : 1 0;
@@ -277,8 +278,7 @@
background-color : #DDDDDD; background-color : #DDDDDD;
border-radius : 0.5em; border-radius : 0.5em;
.icon { .icon { #groupedIcon; }
#groupedIcon; }
} }
.input-group { .input-group {
@@ -294,16 +294,29 @@
height : 100%; height : 100%;
} }
.invalid:focus { background-color : pink; } .input-group {
height : ~'calc(.9em + 4px + .6em)';
.icon { input { border-radius : 0.5em 0 0 0.5em; }
#groupedIcon;
top : -0.54em;
right : 1px;
height : 97%;
font-size : 0.8em;
i { font-size : 1.125em; } input:last-child { border-radius : 0.5em; }
.value {
width : 7.5vw;
min-width : 75px;
height : 100%;
}
.invalid:focus { background-color : pink; }
.icon {
#groupedIcon;
top : -0.54em;
right : 1px;
height : 97%;
i { font-size : 1.125em; }
}
} }
} }
} }

View File

@@ -128,7 +128,7 @@ const StringArrayEditor = createClass({
return <div className='field'> return <div className='field'>
<label>{this.props.label}</label> <label>{this.props.label}</label>
<div style={{ flex: '1 0' }}> <div style={{ flex: '1 0' }} className='value'>
<div className='list'> <div className='list'>
{valueElements} {valueElements}
<div className='input-group'> <div className='input-group'>

View File

@@ -1,34 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const MAX_TITLE_LENGTH = 50;
const EditTitle = createClass({
displayName : 'EditTitleNavItem',
getDefaultProps : function() {
return {
title : '',
onChange : function(){}
};
},
handleChange : function(e){
if(e.target.value.length > MAX_TITLE_LENGTH) return;
this.props.onChange(e.target.value);
},
render : function(){
return <Nav.item className='editTitle'>
<input placeholder='Brew Title' type='text' value={this.props.title} onChange={this.handleChange} />
<div className={cx('charCount', { 'max': this.props.title.length >= MAX_TITLE_LENGTH })}>
{this.props.title.length}/{MAX_TITLE_LENGTH}
</div>
</Nav.item>;
},
});
module.exports = EditTitle;

View File

@@ -36,7 +36,7 @@ const RecentItems = createClass({
//== Add current brew to appropriate recent items list (depending on storageKey) ==// //== Add current brew to appropriate recent items list (depending on storageKey) ==//
if(this.props.storageKey == 'edit'){ if(this.props.storageKey == 'edit'){
let editId = this.props.brew.editId; let editId = this.props.brew.editId;
if(this.props.brew.googleId){ if(this.props.brew.googleId && !this.props.brew.stubbed){
editId = `${this.props.brew.googleId}${this.props.brew.editId}`; editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
} }
edited = _.filter(edited, (brew)=>{ edited = _.filter(edited, (brew)=>{
@@ -51,7 +51,7 @@ const RecentItems = createClass({
} }
if(this.props.storageKey == 'view'){ if(this.props.storageKey == 'view'){
let shareId = this.props.brew.shareId; let shareId = this.props.brew.shareId;
if(this.props.brew.googleId){ if(this.props.brew.googleId && !this.props.brew.stubbed){
shareId = `${this.props.brew.googleId}${this.props.brew.shareId}`; shareId = `${this.props.brew.googleId}${this.props.brew.shareId}`;
} }
viewed = _.filter(viewed, (brew)=>{ viewed = _.filter(viewed, (brew)=>{
@@ -83,7 +83,7 @@ const RecentItems = createClass({
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]'); let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
if(this.props.storageKey == 'edit') { if(this.props.storageKey == 'edit') {
let prevEditId = prevProps.brew.editId; let prevEditId = prevProps.brew.editId;
if(prevProps.brew.googleId){ if(prevProps.brew.googleId && !this.props.brew.stubbed){
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`; prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
} }
@@ -91,7 +91,7 @@ const RecentItems = createClass({
return brew.id !== prevEditId; return brew.id !== prevEditId;
}); });
let editId = this.props.brew.editId; let editId = this.props.brew.editId;
if(this.props.brew.googleId){ if(this.props.brew.googleId && !this.props.brew.stubbed){
editId = `${this.props.brew.googleId}${this.props.brew.editId}`; editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
} }
edited.unshift({ edited.unshift({

View File

@@ -1,44 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';
const RedditShare = createClass({
displayName : 'RedditShareNavItem',
getDefaultProps : function() {
return {
brew : {
title : '',
sharedId : '',
text : ''
}
};
},
getText : function(){
},
handleClick : function(){
const url = [
MAIN_URL,
`title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`,
`text=${encodeURIComponent(this.props.brew.text)}`
].join('&');
window.open(url, '_blank');
},
render : function(){
return <Nav.item icon='fa-reddit-alien' color='red' onClick={this.handleClick}>
share on reddit
</Nav.item>;
},
});
module.exports = RedditShare;

View File

@@ -32,7 +32,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers
const googleDriveIcon = require('../../googleDrive.svg'); const googleDriveIcon = require('../../googleDrive.svg');
const SAVE_TIMEOUT = 3000; const SAVE_TIMEOUT = 10000;
const EditPage = createClass({ const EditPage = createClass({
displayName : 'EditPage', displayName : 'EditPage',

View File

@@ -25,7 +25,8 @@ const SharePage = createClass({
getInitialState : function() { getInitialState : function() {
return { return {
themeBundle : {} themeBundle : {},
currentBrewRendererPageNum : 1
}; };
}, },
@@ -39,6 +40,10 @@ const SharePage = createClass({
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleControlKeys : function(e){ handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return; if(!(e.ctrlKey || e.metaKey)) return;
const P_KEY = 80; const P_KEY = 80;
@@ -114,9 +119,12 @@ const SharePage = createClass({
<BrewRenderer <BrewRenderer
text={this.props.brew.text} text={this.props.brew.text}
style={this.props.brew.style} style={this.props.brew.style}
lang={this.props.brew.lang}
renderer={this.props.brew.renderer} renderer={this.props.brew.renderer}
theme={this.props.brew.theme} theme={this.props.brew.theme}
themeBundle={this.state.themeBundle} themeBundle={this.state.themeBundle}
onPageChange={this.handleBrewRendererPageChange}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
allowPrint={true} allowPrint={true}
/> />
</div> </div>

View File

@@ -1,3 +1,5 @@
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
/*eslint max-params:["warn", { max: 10 }], */
require('./vaultPage.less'); require('./vaultPage.less');
const React = require('react'); const React = require('react');
@@ -18,13 +20,15 @@ const request = require('../../utils/request-middleware.js');
const VaultPage = (props)=>{ const VaultPage = (props)=>{
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1); const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
const [sortState, setSort] = useState(props.query.sort || 'title');
const [dirState, setdir] = useState(props.query.dir || 'asc');
//Response state //Response state
const [brewCollection, setBrewCollection] = useState(null); const [brewCollection, setBrewCollection] = useState(null);
const [totalBrews, setTotalBrews] = useState(null); const [totalBrews, setTotalBrews] = useState(null);
const [searching, setSearching] = useState(false); const [searching, setSearching] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const titleRef = useRef(null); const titleRef = useRef(null);
const authorRef = useRef(null); const authorRef = useRef(null);
const countRef = useRef(null); const countRef = useRef(null);
@@ -34,7 +38,7 @@ const VaultPage = (props)=>{
useEffect(()=>{ useEffect(()=>{
disableSubmitIfFormInvalid(); disableSubmitIfFormInvalid();
loadPage(pageState, true); loadPage(pageState, true, props.query.sort, props.query.dir);
}, []); }, []);
const updateStateWithBrews = (brews, page)=>{ const updateStateWithBrews = (brews, page)=>{
@@ -43,7 +47,7 @@ const VaultPage = (props)=>{
setSearching(false); setSearching(false);
}; };
const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page)=>{ const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page, sort, dir)=>{
const url = new URL(window.location.href); const url = new URL(window.location.href);
const urlParams = new URLSearchParams(url.search); const urlParams = new URLSearchParams(url.search);
@@ -53,21 +57,23 @@ const VaultPage = (props)=>{
urlParams.set('v3', v3Value); urlParams.set('v3', v3Value);
urlParams.set('legacy', legacyValue); urlParams.set('legacy', legacyValue);
urlParams.set('page', page); urlParams.set('page', page);
urlParams.set('sort', sort);
urlParams.set('dir', dir);
url.search = urlParams.toString(); url.search = urlParams.toString();
window.history.replaceState(null, '', url.toString()); window.history.replaceState(null, '', url.toString());
}; };
const performSearch = async (title, author, count, v3, legacy, page)=>{ const performSearch = async (title, author, count, v3, legacy, page, sort, dir)=>{
updateUrl(title, author, count, v3, legacy, page); updateUrl(title, author, count, v3, legacy, page, sort, dir);
const response = await request.get( const response = await request
`/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}` .get(`/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}&sort=${sort}&dir=${dir}`)
).catch((error)=>{ .catch((error)=>{
console.log('error at loadPage: ', error); console.log('error at loadPage: ', error);
setError(error); setError(error);
updateStateWithBrews([], 1); updateStateWithBrews([], 1);
}); });
if(response.ok) if(response.ok)
updateStateWithBrews(response.body.brews, page); updateStateWithBrews(response.body.brews, page);
@@ -76,9 +82,8 @@ const VaultPage = (props)=>{
const loadTotal = async (title, author, v3, legacy)=>{ const loadTotal = async (title, author, v3, legacy)=>{
setTotalBrews(null); setTotalBrews(null);
const response = await request.get( const response = await request.get(`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}`)
`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}` .catch((error)=>{
).catch((error)=>{
console.log('error at loadTotal: ', error); console.log('error at loadTotal: ', error);
setError(error); setError(error);
updateStateWithBrews([], 1); updateStateWithBrews([], 1);
@@ -88,9 +93,8 @@ const VaultPage = (props)=>{
setTotalBrews(response.body.totalBrews); setTotalBrews(response.body.totalBrews);
}; };
const loadPage = async (page, updateTotal)=>{ const loadPage = async (page, updateTotal, sort, dir)=>{
if(!validateForm()) if(!validateForm()) return;
return;
setSearching(true); setSearching(true);
setError(null); setError(null);
@@ -100,8 +104,14 @@ const VaultPage = (props)=>{
const count = countRef.current.value || 10; const count = countRef.current.value || 10;
const v3 = v3Ref.current.checked != false; const v3 = v3Ref.current.checked != false;
const legacy = legacyRef.current.checked != false; const legacy = legacyRef.current.checked != false;
const sortOption = sort || 'title';
const dirOption = dir || 'asc';
const pageProp = page || 1;
performSearch(title, author, count, v3, legacy, page); setSort(sortOption);
setdir(dirOption);
performSearch(title, author, count, v3, legacy, pageProp, sortOption, dirOption);
if(updateTotal) if(updateTotal)
loadTotal(title, author, v3, legacy); loadTotal(title, author, v3, legacy);
@@ -248,6 +258,33 @@ const VaultPage = (props)=>{
</div> </div>
); );
const renderSortOption = (optionTitle, optionValue)=>{
const oppositeDir = dirState === 'asc' ? 'desc' : 'asc';
return (
<div className={`sort-option ${sortState === optionValue ? `active` : ''}`}>
<button onClick={()=>loadPage(1, false, optionValue, oppositeDir)}>
{optionTitle}
</button>
{sortState === optionValue && (
<i className={`sortDir fas ${dirState === 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`} />
)}
</div>
);
};
const renderSortBar = ()=>{
return (
<div className='sort-container'>
{renderSortOption('Title', 'title', props.query.dir)}
{renderSortOption('Created Date', 'createdAt', props.query.dir)}
{renderSortOption('Updated Date', 'updatedAt', props.query.dir)}
{renderSortOption('Views', 'views', props.query.dir)}
</div>
);
};
const renderPaginationControls = ()=>{ const renderPaginationControls = ()=>{
if(!totalBrews) return null; if(!totalBrews) return null;
@@ -271,10 +308,8 @@ const VaultPage = (props)=>{
.map((_, index)=>( .map((_, index)=>(
<a <a
key={startPage + index} key={startPage + index}
className={`pageNumber ${ className={`pageNumber ${pageState === startPage + index ? 'currentPage' : ''}`}
pageState === startPage + index ? 'currentPage' : '' onClick={()=>loadPage(startPage + index, false, sortState, dirState)}
}`}
onClick={()=>loadPage(startPage + index, false)}
> >
{startPage + index} {startPage + index}
</a> </a>
@@ -284,7 +319,7 @@ const VaultPage = (props)=>{
<div className='paginationControls'> <div className='paginationControls'>
<button <button
className='previousPage' className='previousPage'
onClick={()=>loadPage(pageState - 1, false)} onClick={()=>loadPage(pageState - 1, false, sortState, dirState)}
disabled={pageState === startPage} disabled={pageState === startPage}
> >
<i className='fa-solid fa-chevron-left'></i> <i className='fa-solid fa-chevron-left'></i>
@@ -293,7 +328,7 @@ const VaultPage = (props)=>{
{startPage > 1 && ( {startPage > 1 && (
<a <a
className='pageNumber firstPage' className='pageNumber firstPage'
onClick={()=>loadPage(1, false)} onClick={()=>loadPage(1, false, sortState, dirState)}
> >
1 ... 1 ...
</a> </a>
@@ -302,7 +337,7 @@ const VaultPage = (props)=>{
{endPage < totalPages && ( {endPage < totalPages && (
<a <a
className='pageNumber lastPage' className='pageNumber lastPage'
onClick={()=>loadPage(totalPages, false)} onClick={()=>loadPage(totalPages, false, sortState, dirState)}
> >
... {totalPages} ... {totalPages}
</a> </a>
@@ -310,7 +345,7 @@ const VaultPage = (props)=>{
</ol> </ol>
<button <button
className='nextPage' className='nextPage'
onClick={()=>loadPage(pageState + 1, false)} onClick={()=>loadPage(pageState + 1, false, sortState, dirState)}
disabled={pageState === totalPages} disabled={pageState === totalPages}
> >
<i className='fa-solid fa-chevron-right'></i> <i className='fa-solid fa-chevron-right'></i>
@@ -385,6 +420,7 @@ const VaultPage = (props)=>{
<div className='form dataGroup'>{renderForm()}</div> <div className='form dataGroup'>{renderForm()}</div>
<div className='resultsContainer dataGroup'> <div className='resultsContainer dataGroup'>
{renderSortBar()}
{renderFoundBrews()} {renderFoundBrews()}
</div> </div>
</SplitPane> </SplitPane>

View File

@@ -6,8 +6,8 @@
*:not(input) { user-select : none; } *:not(input) { user-select : none; }
.content { .content {
height : 100%;
background : #2C3E50; background : #2C3E50;
height: 100%;
.dataGroup { .dataGroup {
width : 100%; width : 100%;
@@ -27,9 +27,9 @@
code { code {
padding-inline : 5px; padding-inline : 5px;
font-family : monospace;
background : lightgrey; background : lightgrey;
border-radius : 5px; border-radius : 5px;
font-family : monospace;
} }
h1, h2, h3, h4 { h1, h2, h3, h4 {
@@ -165,6 +165,48 @@
color : white; 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 { .foundBrews {
position : relative; position : relative;
width : 100%; width : 100%;
@@ -236,15 +278,15 @@
width : 47%; width : 47%;
margin-right : 40px; margin-right : 40px;
color : black; color : black;
isolation:isolate; isolation : isolate;
&:after { &::after {
position:absolute; position : absolute;
inset:0; inset : 0;
display:block; z-index : -2;
content:''; display : block;
content : '';
background-image : url('/assets/parchmentBackground.jpg'); background-image : url('/assets/parchmentBackground.jpg');
z-index:-1;
} }
&:nth-child(even of .brewItem) { margin-right : 0; } &:nth-child(even of .brewItem) { margin-right : 0; }
@@ -257,28 +299,24 @@
color : var(--HB_Color_HeaderText); color : var(--HB_Color_HeaderText);
} }
.info { .info {
position : relative;
z-index : 2;
font-family : 'ScalySansRemake'; font-family : 'ScalySansRemake';
font-size : 1.2em; font-size : 1.2em;
position:relative;
z-index:2;
>span { >span {
margin-right : 12px; margin-right : 12px;
line-height : 1.5em; line-height : 1.5em;
} }
} }
.links { .links { z-index : 2; }
z-index:2;
}
hr { hr {
margin: 0px; margin : 0px;
visibility: hidden; visibility : hidden;
} }
.thumbnail { .thumbnail { z-index : -1; }
z-index:1;
}
} }
.paginationControls { .paginationControls {

View File

@@ -6,5 +6,7 @@
"enable_v3" : true, "enable_v3" : true,
"enable_themes" : true, "enable_themes" : true,
"local_environments" : ["docker", "local"], "local_environments" : ["docker", "local"],
"publicUrl" : "https://homebrewery.naturalcrit.com" "publicUrl" : "https://homebrewery.naturalcrit.com",
"hb_images" : null,
"hb_fonts" : null
} }

1473
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -86,20 +86,20 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.7",
"@babel/plugin-transform-runtime": "^7.25.4", "@babel/plugin-transform-runtime": "^7.25.7",
"@babel/preset-env": "^7.25.4", "@babel/preset-env": "^7.25.7",
"@babel/preset-react": "^7.24.7", "@babel/preset-react": "^7.25.7",
"@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",
"codemirror": "^5.65.6", "codemirror": "^5.65.6",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.7",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3", "dedent-tabs": "^0.10.3",
"dompurify": "^3.1.6", "dompurify": "^3.1.7",
"expr-eval": "^2.0.2", "expr-eval": "^2.0.2",
"express": "^4.21.0", "express": "^4.21.1",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.8", "express-static-gzip": "2.1.8",
"fs-extra": "11.2.0", "fs-extra": "11.2.0",
@@ -114,7 +114,7 @@
"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.6.2", "mongoose": "^8.7.0",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"nconf": "^0.12.1", "nconf": "^0.12.1",
"react": "^18.3.1", "react": "^18.3.1",
@@ -126,16 +126,16 @@
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/stylelint-plugin": "^3.0.1", "@stylistic/stylelint-plugin": "^3.1.1",
"eslint": "^9.10.0", "eslint": "^9.12.0",
"eslint-plugin-jest": "^28.8.3", "eslint-plugin-jest": "^28.8.3",
"eslint-plugin-react": "^7.36.1", "eslint-plugin-react": "^7.37.1",
"globals": "^15.9.0", "globals": "^15.10.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.9.0",
"stylelint-config-recess-order": "^5.1.0", "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

@@ -8,6 +8,8 @@ const express = require('express');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const app = express(); const app = express();
const config = require('./config.js'); const config = require('./config.js');
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');
@@ -451,6 +453,10 @@ if(isLocalEnvironment){
}); });
} }
// Add Static Local Paths
app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
//Vault Page //Vault Page
app.get('/vault', asyncHandler(async(req, res, next)=>{ app.get('/vault', asyncHandler(async(req, res, next)=>{
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,
@@ -520,7 +526,7 @@ app.use(async (err, req, res, next)=>{
err.originalUrl = req.originalUrl; err.originalUrl = req.originalUrl;
console.error(err); console.error(err);
if(err.originalUrl?.startsWith('/api/')) { if(err.originalUrl?.startsWith('/api')) {
// console.log('API error'); // console.log('API error');
res.status(err.status || err.response?.status || 500).send(err); res.status(err.status || err.response?.status || 500).send(err);
return; return;

View File

@@ -172,7 +172,6 @@ const GoogleActions = {
}) })
.catch((err)=>{ .catch((err)=>{
console.log('Error saving to google'); console.log('Error saving to google');
console.error(err);
throw (err); throw (err);
}); });
@@ -211,7 +210,6 @@ const GoogleActions = {
}) })
.catch((err)=>{ .catch((err)=>{
console.log('Error while creating new Google brew'); console.log('Error while creating new Google brew');
console.error(err);
throw (err); throw (err);
}); });

View File

@@ -242,11 +242,8 @@ const api = {
let googleId, saved; let googleId, saved;
if(saveToGoogle) { if(saveToGoogle) {
googleId = await api.newGoogleBrew(req.account, newHomebrew, res) googleId = await api.newGoogleBrew(req.account, newHomebrew, res);
.catch((err)=>{
console.error(err);
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
});
if(!googleId) return; if(!googleId) return;
api.excludeStubProps(newHomebrew); api.excludeStubProps(newHomebrew);
newHomebrew.googleId = googleId; newHomebrew.googleId = googleId;
@@ -351,19 +348,13 @@ const api = {
brew.googleId = undefined; brew.googleId = undefined;
} else if(!brew.googleId && saveToGoogle) { } else if(!brew.googleId && saveToGoogle) {
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew // If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res) brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res);
.catch((err)=>{
console.error(err);
res.status(err.status || err.response.status).send(err.message || err);
});
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));
.catch((err)=>{
console.error(err);
res.status(err?.response?.status || 500).send(err);
});
if(!updated) return; if(!updated) return;
} }

View File

@@ -560,16 +560,6 @@ brew`);
views : 0 views : 0
}); });
}); });
it('should handle google error', async()=>{
google.newGoogleBrew = jest.fn(()=>{
throw 'err';
});
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.send).toHaveBeenCalledWith('err');
});
}); });
describe('deleteGoogleBrew', ()=>{ describe('deleteGoogleBrew', ()=>{

View File

@@ -1,8 +1,12 @@
const config = require('../config.js');
const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
module.exports = (req, res, next)=>{ module.exports = (req, res, next)=>{
const isImageRequest = req.get('Accept')?.split(',') const isImageRequest = req.get('Accept')?.split(',')
?.filter((h)=>!h.includes('q=')) ?.filter((h)=>!h.includes('q='))
?.every((h)=>/image\/.*/.test(h)); ?.every((h)=>/image\/.*/.test(h));
if(isImageRequest) { if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
return res.status(406).send({ return res.status(406).send({
message : 'Request for image at this URL is not supported' message : 'Request for image at this URL is not supported'
}); });

View File

@@ -29,12 +29,18 @@ const rendererConditions = (legacy, v3)=>{
return {}; // If all renderers selected, renderer field not needed in query for speed return {}; // If all renderers selected, renderer field not needed in query for speed
}; };
const sortConditions = (sort, dir) => {
return { [sort]: dir === 'asc' ? 1 : -1 };
};
const findBrews = async (req, res)=>{ const findBrews = async (req, res)=>{
const title = req.query.title || ''; const title = req.query.title || '';
const author = req.query.author || ''; const author = req.query.author || '';
const page = Math.max(parseInt(req.query.page) || 1, 1); const page = Math.max(parseInt(req.query.page) || 1, 1);
const count = Math.max(parseInt(req.query.count) || 20, 10); const count = Math.max(parseInt(req.query.count) || 20, 10);
const skip = (page - 1) * count; const skip = (page - 1) * count;
const sort = req.query.sort || 'title';
const dir = req.query.dir || 'asc';
const combinedQuery = { const combinedQuery = {
$and : [ $and : [
@@ -54,6 +60,7 @@ const findBrews = async (req, res)=>{
}; };
await HomebrewModel.find(combinedQuery, projection) await HomebrewModel.find(combinedQuery, projection)
.sort(sortConditions(sort, dir))
.skip(skip) .skip(skip)
.limit(count) .limit(count)
.maxTimeMS(5000) .maxTimeMS(5000)