mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-11-10 02:08:45 +00:00
Merge branch 'master' into issue_5048
This commit is contained in:
+22
-1
@@ -90,9 +90,30 @@ pre {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Tuesday 10/20/2026 - v3.24.2
|
||||
{{taskList
|
||||
##### G-Ambatte
|
||||
* [x] Hotfix for memory leak on server
|
||||
|
||||
Fixes issue [#5090](https://github.com/naturalcrit/homebrewery/issues/5090)
|
||||
|
||||
##### Calculuschild
|
||||
* [x] Fix brew variables not working across pages
|
||||
|
||||
* [x] Allow brew variable definitions to be on adjacent lines (no blank line required anymore)
|
||||
|
||||
##### 5e-Cleric
|
||||
* [x] Partial rollout of some site UI changes (includes alternate views and darkmode in {{openSans **BREWS** :fas_beer_mug_empty:}} page, and changes to {{openSans **VAULT** :fas_dungeon:}})
|
||||
|
||||
##### Abquintic, Calculuschild, 5e-Cleric
|
||||
* [x] Multiple background fixes and code cleanups
|
||||
}}
|
||||
|
||||
|
||||
### Tuesday 9/29/2026 - v3.24.1
|
||||
{{taskList
|
||||
##### calculuschild
|
||||
@@ -169,7 +190,7 @@ Fixes issue [#3840](https://github.com/naturalcrit/homebrewery/issues/3840)
|
||||
|
||||
Fixes issue [#2037](https://github.com/naturalcrit/homebrewery/issues/2037)
|
||||
|
||||
* [x] Remember editor theme across tabs
|
||||
* [x] Remember editor theme across tabs, and new {{openSans **SETTINGS :fas_gear:**}} tab with editor settings
|
||||
|
||||
Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858)
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
import '../uiEditor.less';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
import React, { useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Combobox from '@components/combobox.jsx';
|
||||
import TagInput from '../tagInput/tagInput.jsx';
|
||||
|
||||
import request from '../../utils/request-middleware.js';
|
||||
import Themes from '@themes/themes.json';
|
||||
import validations from './validations.js';
|
||||
|
||||
import Combobox from '@components/combobox.jsx';
|
||||
import TagInput from '../tagInput/tagInput.jsx';
|
||||
|
||||
import homebreweryThumbnail from '../../thumbnail.png';
|
||||
|
||||
const callIfExists = (val, fn, ...args)=>{
|
||||
@@ -18,48 +18,28 @@ const callIfExists = (val, fn, ...args)=>{
|
||||
}
|
||||
};
|
||||
|
||||
const MetadataEditor = createReactClass({
|
||||
displayName : 'MetadataEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
metadata : {
|
||||
editId : null,
|
||||
shareId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
thumbnail : '',
|
||||
tags : [],
|
||||
published : false,
|
||||
authors : [],
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB',
|
||||
lang : 'en'
|
||||
},
|
||||
|
||||
onChange : ()=>{},
|
||||
reportError : ()=>{}
|
||||
};
|
||||
const MetadataEditor = ({
|
||||
metadata = {
|
||||
shareId : null,
|
||||
editId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
tags : [],
|
||||
lang : 'en',
|
||||
renderer : 'legacy',
|
||||
authors : [],
|
||||
published : false,
|
||||
thumbnail : '',
|
||||
theme : '5ePHB',
|
||||
},
|
||||
onChange = ()=>{},
|
||||
reportError = ()=>{},
|
||||
userThemes = {},
|
||||
themeBundle = {}
|
||||
})=>{
|
||||
const [showThumbnail, setShowThumbnail] = useState(true);
|
||||
|
||||
getInitialState : function(){
|
||||
return {
|
||||
isOwner : global.account?.username && global.account?.username === this.props.metadata?.authors[0],
|
||||
showThumbnail : true
|
||||
};
|
||||
},
|
||||
|
||||
toggleThumbnailDisplay : function(){
|
||||
this.setState({
|
||||
showThumbnail : !this.state.showThumbnail
|
||||
});
|
||||
},
|
||||
|
||||
renderThumbnail : function(){
|
||||
if(!this.state.showThumbnail) return;
|
||||
return <img className='thumbnail-preview' src={this.props.metadata.thumbnail || homebreweryThumbnail}></img>;
|
||||
},
|
||||
|
||||
handleFieldChange : function(name, e){
|
||||
const handleFieldChange = (name, e)=>{
|
||||
// load validation rules, and check input value against them
|
||||
const inputRules = validations[name] ?? [];
|
||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||
@@ -72,8 +52,8 @@ const MetadataEditor = createReactClass({
|
||||
// if no validation rules, save to props
|
||||
if(validationErr.length === 0){
|
||||
callIfExists(e.target, 'setCustomValidity', '');
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
onChange({
|
||||
...metadata,
|
||||
[name] : e.target.value
|
||||
});
|
||||
return true;
|
||||
@@ -86,45 +66,37 @@ const MetadataEditor = createReactClass({
|
||||
debouncedReportValidity(e.target, errMessage);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
handleRenderer : function(renderer, e){
|
||||
};
|
||||
const handleRenderer = (renderer, e)=>{
|
||||
if(e.target.checked){
|
||||
this.props.metadata.renderer = renderer;
|
||||
metadata.renderer = renderer;
|
||||
if(renderer == 'legacy')
|
||||
this.props.metadata.theme = '5ePHB';
|
||||
metadata.theme = '5ePHB';
|
||||
}
|
||||
this.props.onChange(this.props.metadata, 'renderer');
|
||||
},
|
||||
|
||||
handlePublish : function(val){
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
onChange(metadata, 'renderer');
|
||||
};
|
||||
const handlePublish = (val)=>{
|
||||
onChange({
|
||||
...metadata,
|
||||
published : val
|
||||
});
|
||||
},
|
||||
|
||||
handleTheme : function(theme){
|
||||
this.props.metadata.renderer = theme.renderer;
|
||||
this.props.metadata.theme = theme.path;
|
||||
|
||||
this.props.onChange(this.props.metadata, 'theme');
|
||||
},
|
||||
|
||||
handleThemeWritein : function(e) {
|
||||
};
|
||||
const handleTheme = (theme)=>{
|
||||
metadata.renderer = theme.renderer;
|
||||
metadata.theme = theme.path;
|
||||
onChange(metadata, 'theme');
|
||||
};
|
||||
const handleThemeWritein = (e)=>{
|
||||
const shareId = e.target.value.split('/').pop(); //Extract just the ID if a URL was pasted in
|
||||
this.props.metadata.theme = shareId;
|
||||
|
||||
this.props.onChange(this.props.metadata, 'theme');
|
||||
},
|
||||
|
||||
handleLanguage : function(languageCode){
|
||||
this.props.metadata.lang = languageCode;
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
handleDelete : function(){
|
||||
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
|
||||
metadata.theme = shareId;
|
||||
onChange(metadata, 'theme');
|
||||
};
|
||||
const handleLanguage = (languageCode)=>{
|
||||
metadata.lang = languageCode;
|
||||
onChange(metadata);
|
||||
};
|
||||
const handleDelete = ()=>{
|
||||
if(metadata.authors && metadata.authors.length <= 1){
|
||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||
} else {
|
||||
@@ -132,54 +104,39 @@ const MetadataEditor = createReactClass({
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
||||
request.delete(`/api/${metadata.googleId ?? ''}${metadata.editId}`)
|
||||
.send()
|
||||
.end((err, res)=>{
|
||||
if(err) {
|
||||
this.props.reportError(err);
|
||||
reportError(err);
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleDeleteAuthor : function(author){
|
||||
};
|
||||
const handleDeleteAuthor = (author)=>{
|
||||
if(!confirm('Are you sure you want to remove this author? They will lose all edit access to this brew, and it will dissapear from their userpage.')) return;
|
||||
if(!this.props.metadata.authors.includes(author)) return;
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
authors : this.props.metadata.authors.filter((a)=>a !== author)
|
||||
if(!metadata.authors.includes(author)) return;
|
||||
onChange({
|
||||
...metadata,
|
||||
authors : metadata.authors.filter((a)=>a !== author)
|
||||
});
|
||||
},
|
||||
|
||||
renderPublish : function(){
|
||||
if(this.props.metadata.published){
|
||||
return <button id='publish-button' className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||
};
|
||||
const renderPublish = ()=>{
|
||||
if(metadata.published){
|
||||
return <button id='publish-button' className='unpublish' onClick={()=>handlePublish(false)}>
|
||||
<i className='fas fa-ban' aria-hidden='true' /> unpublish
|
||||
</button>;
|
||||
} else {
|
||||
return <button id='publish-button' className='publish' onClick={()=>this.handlePublish(true)}>
|
||||
return <button id='publish-button' className='publish' onClick={()=>handlePublish(true)}>
|
||||
<i className='fas fa-globe' aria-hidden='true' /> publish
|
||||
</button>;
|
||||
}
|
||||
},
|
||||
|
||||
renderDelete : function(){
|
||||
if(!this.props.metadata.editId) return;
|
||||
|
||||
return <div className='field delete'>
|
||||
<label htmlFor='delete-button'>delete</label>
|
||||
<div className='value'>
|
||||
<button id='delete-button' onClick={this.handleDelete}>
|
||||
<i className='fas fa-trash-alt' /> delete brew
|
||||
</button>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderAuthors : function(){
|
||||
const authors = this.props.metadata.authors;
|
||||
if(!this.state.isOwner || authors.length < 2) return (
|
||||
};
|
||||
const renderAuthors = ()=>{
|
||||
const authors = metadata.authors;
|
||||
const isOwner = global.account?.username && global.account?.username === authors[0];
|
||||
if(!isOwner || authors.length < 2) return (
|
||||
<div className='field authors'>
|
||||
<label>authors</label>
|
||||
<div className='value'>
|
||||
@@ -214,7 +171,7 @@ const MetadataEditor = createReactClass({
|
||||
{author}
|
||||
</a>
|
||||
<button
|
||||
onClick={()=>this.handleDeleteAuthor(author)}
|
||||
onClick={()=>handleDeleteAuthor(author)}
|
||||
className='delete'
|
||||
title={`Remove ${author} as an author`}
|
||||
>
|
||||
@@ -225,14 +182,13 @@ const MetadataEditor = createReactClass({
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
renderThemeDropdown : function(){
|
||||
const mergedThemes = _.merge(Themes, this.props.userThemes);
|
||||
};
|
||||
const renderThemeDropdown = ()=>{
|
||||
const mergedThemes = _.merge(Themes, userThemes);
|
||||
|
||||
const listThemes = (renderer)=>{
|
||||
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
|
||||
if(theme.path == this.props.metadata.shareId) return;
|
||||
if(theme.path == metadata.shareId) return;
|
||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||
return <div className='item' key={`${renderer}_${theme.name}`} value={`${theme.author ?? renderer} : ${theme.name}`} data={theme} title={''}>
|
||||
@@ -248,8 +204,8 @@ const MetadataEditor = createReactClass({
|
||||
}).filter(Boolean);
|
||||
};
|
||||
|
||||
const currentRenderer = this.props.metadata.renderer;
|
||||
const currentThemeDisplay = this.props.themeBundle?.name ? `${this.props.themeBundle.author ?? currentRenderer} : ${this.props.themeBundle.name}` : 'No Theme Selected';
|
||||
const currentRenderer = metadata.renderer;
|
||||
const currentThemeDisplay = themeBundle?.name ? `${themeBundle.author ?? currentRenderer} : ${themeBundle.name}` : 'No Theme Selected';
|
||||
let dropdown;
|
||||
|
||||
if(currentRenderer == 'legacy') {
|
||||
@@ -265,11 +221,11 @@ const MetadataEditor = createReactClass({
|
||||
className='themes-dropdown'
|
||||
default={currentThemeDisplay}
|
||||
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
||||
onSelect={(value)=>this.handleTheme(value)}
|
||||
onSelect={(value)=>handleTheme(value)}
|
||||
onEntry={(e)=>{
|
||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||
if(this.handleFieldChange('theme', e))
|
||||
this.handleThemeWritein(e);
|
||||
if(handleFieldChange('theme', e))
|
||||
handleThemeWritein(e);
|
||||
}}
|
||||
options={listThemes(currentRenderer)}
|
||||
autoSuggest={{
|
||||
@@ -285,9 +241,8 @@ const MetadataEditor = createReactClass({
|
||||
<label htmlFor='combobox-themes'>theme</label>
|
||||
{dropdown}
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderLanguageDropdown : function(){
|
||||
};
|
||||
const renderLanguageDropdown = ()=>{
|
||||
const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant'];
|
||||
const listLanguages = ()=>{
|
||||
return _.map(langCodes.sort(), (code, index)=>{
|
||||
@@ -306,12 +261,12 @@ const MetadataEditor = createReactClass({
|
||||
<Combobox trigger='click'
|
||||
id='combobox-language'
|
||||
className='language-dropdown'
|
||||
default={this.props.metadata.lang || ''}
|
||||
default={metadata.lang || ''}
|
||||
placeholder='en'
|
||||
onSelect={(value)=>this.handleLanguage(value)}
|
||||
onSelect={(value)=>handleLanguage(value)}
|
||||
onEntry={(e)=>{
|
||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||
this.handleFieldChange('lang', e);
|
||||
handleFieldChange('lang', e);
|
||||
}}
|
||||
options={listLanguages()}
|
||||
autoSuggest={{
|
||||
@@ -321,11 +276,9 @@ const MetadataEditor = createReactClass({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderRenderOptions : function(){
|
||||
};
|
||||
const renderRenderOptions = ()=>{
|
||||
return <div className='field renderers'>
|
||||
<label>Renderer</label>
|
||||
<div className='value'>
|
||||
@@ -334,8 +287,8 @@ const MetadataEditor = createReactClass({
|
||||
type='radio'
|
||||
value = 'legacy'
|
||||
name = 'renderer'
|
||||
checked={this.props.metadata.renderer === 'legacy'}
|
||||
onChange={(e)=>this.handleRenderer('legacy', e)} />
|
||||
checked={metadata.renderer === 'legacy'}
|
||||
onChange={(e)=>handleRenderer('legacy', e)} />
|
||||
Legacy
|
||||
</label>
|
||||
|
||||
@@ -344,74 +297,81 @@ const MetadataEditor = createReactClass({
|
||||
type='radio'
|
||||
value = 'V3'
|
||||
name = 'renderer'
|
||||
checked={this.props.metadata.renderer === 'V3'}
|
||||
onChange={(e)=>this.handleRenderer('V3', e)} />
|
||||
checked={metadata.renderer === 'V3'}
|
||||
onChange={(e)=>handleRenderer('V3', e)} />
|
||||
V3
|
||||
</label>
|
||||
<small><a href='/legacy' target='_blank' rel='noopener noreferrer'>Click here to see the demo page for the old Legacy renderer!</a></small>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
};
|
||||
|
||||
render : function(){
|
||||
return <div className='metadataEditor uiEditor'>
|
||||
return (
|
||||
<div className='metadataEditor uiEditor'>
|
||||
<h1>Properties Editor</h1>
|
||||
|
||||
<div className='field title'>
|
||||
<label htmlFor='title_field'>title</label>
|
||||
<input type='text' id='title_field' className='value'
|
||||
defaultValue={this.props.metadata.title}
|
||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||
<input
|
||||
type='text'
|
||||
id='title_field'
|
||||
className='value'
|
||||
defaultValue={metadata.title}
|
||||
onChange={(e)=>handleFieldChange('title', e)}
|
||||
/>
|
||||
</div>
|
||||
<fieldset className='field-group'>
|
||||
<div className='field-column'>
|
||||
<div className='field description'>
|
||||
<label htmlFor='description_field'>description</label>
|
||||
<textarea id='description_field' defaultValue={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
<textarea
|
||||
id='description_field'
|
||||
defaultValue={metadata.description}
|
||||
className='value'
|
||||
onChange={(e)=>handleFieldChange('description', e)}
|
||||
/>
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label htmlFor='thumbnail_field'>thumbnail</label>
|
||||
<input type='text'
|
||||
<input
|
||||
type='text'
|
||||
id='thumbnail_field'
|
||||
defaultValue={this.props.metadata.thumbnail}
|
||||
defaultValue={metadata.thumbnail}
|
||||
placeholder='https://my.thumbnail.url'
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}
|
||||
aria-label={`${this.state.showThumbnail ? 'hide thumbnail' : 'show thumbnail'}`}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
onChange={(e)=>handleFieldChange('thumbnail', e)}
|
||||
/>
|
||||
<button
|
||||
className='display'
|
||||
onClick={()=>setShowThumbnail(!showThumbnail)}
|
||||
aria-label={`${showThumbnail ? 'hide thumbnail' : 'show thumbnail'}`}>
|
||||
<i className={`fas fa-caret-${showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderThumbnail()}
|
||||
{showThumbnail && <img className='thumbnail-preview' src={metadata.thumbnail || homebreweryThumbnail}></img>}
|
||||
</fieldset>
|
||||
|
||||
<div className='field tags'>
|
||||
<label htmlFor='combobox-tags'>Tags</label>
|
||||
<div className='value' >
|
||||
<div className='value'>
|
||||
<TagInput
|
||||
id='combobox-tags'
|
||||
label='tags'
|
||||
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
|
||||
placeholder='add tag' unique={true}
|
||||
values={this.props.metadata.tags}
|
||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||
placeholder='add tag'
|
||||
unique={true}
|
||||
values={metadata.tags}
|
||||
onChange={(e)=>handleFieldChange('tags', e)}
|
||||
tooltip='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.renderLanguageDropdown()}
|
||||
|
||||
{this.renderThemeDropdown()}
|
||||
|
||||
{this.renderRenderOptions()}
|
||||
{renderLanguageDropdown()}
|
||||
{renderThemeDropdown()}
|
||||
{renderRenderOptions()}
|
||||
|
||||
<h2>Authors</h2>
|
||||
|
||||
{this.renderAuthors()}
|
||||
|
||||
{renderAuthors()}
|
||||
<div className='field invitedAuthors'>
|
||||
<label htmlFor='combobox-invited-authors'>Invited authors</label>
|
||||
<div className='value'>
|
||||
@@ -419,30 +379,39 @@ const MetadataEditor = createReactClass({
|
||||
id='combobox-invited-authors'
|
||||
label='invited authors'
|
||||
valuePatterns={/.+/}
|
||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||
placeholder='invite author' unique={true}
|
||||
validators={[(v)=>!metadata.authors?.includes(v)]}
|
||||
placeholder='invite author'
|
||||
unique={true}
|
||||
tooltip={`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.`}
|
||||
values={this.props.metadata.invitedAuthors}
|
||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
||||
values={metadata.invitedAuthors}
|
||||
onChange={(e)=>handleFieldChange('invitedAuthors', e)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Privacy</h2>
|
||||
|
||||
<div className='field publish'>
|
||||
<label htmlFor='publish-button'>publish</label>
|
||||
<div className='value'>
|
||||
{this.renderPublish()}
|
||||
<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>
|
||||
{renderPublish()}
|
||||
<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>
|
||||
|
||||
{this.renderDelete()}
|
||||
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
{metadata.editId && <div className='field delete'>
|
||||
<label htmlFor='delete-button'>delete</label>
|
||||
<div className='value'>
|
||||
<button id='delete-button' onClick={handleDelete}>
|
||||
<i className='fas fa-trash-alt' /> delete brew
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetadataEditor;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import './uiPage.less';
|
||||
import React from 'react';
|
||||
import createReactClass from 'create-react-class';
|
||||
|
||||
import Nav from '../../../navbar/nav.jsx';
|
||||
import Navbar from '../../../navbar/navbar.jsx';
|
||||
import NewBrewItem from '../../../navbar/newbrew.navitem.jsx';
|
||||
@@ -10,30 +7,22 @@ import RecentNavItems from '../../../navbar/recent.navitem.jsx';
|
||||
const { both: RecentNavItem } = RecentNavItems;
|
||||
import Account from '../../../navbar/account.navitem.jsx';
|
||||
|
||||
const UIPage = ({ brew, children })=><div className='sitePage uiPage'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{brew?.title}</Nav.item>
|
||||
</Nav.section>
|
||||
<Nav.section>
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
const UIPage = createReactClass({
|
||||
displayName : 'UIPage',
|
||||
|
||||
render : function(){
|
||||
return <div className='uiPage sitePage'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
<div className='content'>
|
||||
{children}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
export default UIPage;
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
@import '@themes/assets/assets.less';
|
||||
@import '@sharedStyles/core.less';
|
||||
|
||||
.vaultPage {
|
||||
height : 100%;
|
||||
overflow-y : hidden;
|
||||
|
||||
--bg-clr : white;
|
||||
--bg-image : @backgroundImageAlt;
|
||||
--h1-clr : black;
|
||||
--h2-clr : #000077;
|
||||
--label-clr : black;
|
||||
--input-bg : white;
|
||||
--input-bg-hover : #DDDDDD;
|
||||
--input-text-clr : black;
|
||||
--input-placeholder-clr : grey;
|
||||
|
||||
*:not(input) { user-select : none; }
|
||||
|
||||
.form { background : white; }
|
||||
@@ -13,8 +24,15 @@
|
||||
height : 100%;
|
||||
|
||||
&.form .brewLookup {
|
||||
color : var(--label-clr);
|
||||
background-color : var(--bg-clr);
|
||||
background-image : var(--bg-image);
|
||||
background-size : cover;
|
||||
height:100%;
|
||||
|
||||
position : relative;
|
||||
padding : 50px clamp(20px, 4vw, 50px);
|
||||
|
||||
|
||||
small {
|
||||
font-size : 10pt;
|
||||
@@ -33,6 +51,7 @@
|
||||
h1, h2, h3, h4 {
|
||||
font-family : 'CodeBold';
|
||||
letter-spacing : 2px;
|
||||
color : var(--h1-clr);
|
||||
}
|
||||
|
||||
legend {
|
||||
@@ -87,14 +106,14 @@
|
||||
align-items : center;
|
||||
margin : 10px 0;
|
||||
}
|
||||
select { margin : 0 10px; }
|
||||
|
||||
input {
|
||||
input[type='text'], textarea, select {
|
||||
margin : 0 10px;
|
||||
|
||||
&:invalid { background : rgb(255, 188, 181); }
|
||||
|
||||
|
||||
color : var(--input-text-clr);
|
||||
background-color : var(--input-bg);
|
||||
border : 1px solid var(--input-placeholder-clr);
|
||||
&:hover, :focus { outline : 1px solid var(--input-placeholder-clr); background-color : var(--input-bg-hover); }
|
||||
&::placeholder { color : var(--input-placeholder-clr); }
|
||||
&:invalid { background : #ffbcb5; }
|
||||
}
|
||||
|
||||
#searchButton {
|
||||
|
||||
Generated
+97
-90
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"version": "3.24.1",
|
||||
"version": "3.24.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "homebrewery",
|
||||
"version": "3.24.1",
|
||||
"version": "3.24.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -38,27 +38,27 @@
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent": "^1.7.2",
|
||||
"express": "^5.1.0",
|
||||
"express": "^5.3.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "3.0.2",
|
||||
"fflate": "^0.8.3",
|
||||
"fs-extra": "^11.4.1",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"idb-keyval": "^6.2.5",
|
||||
"js-yaml": "^5.4.3",
|
||||
"js-yaml": "^5.5.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^4.9.1",
|
||||
"lodash": "^4.18.1",
|
||||
"marked-hbfm": "^1.0.1",
|
||||
"marked-hbfm": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.31.0",
|
||||
"mongoose": "^9.11.1",
|
||||
"mongoose": "^9.11.2",
|
||||
"nanoid": "6.0.2",
|
||||
"nconf": "^0.13.0",
|
||||
"node": "^26.11.1",
|
||||
"prettier": "^3.9.9",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"node": "^26.12.0",
|
||||
"prettier": "^3.9.10",
|
||||
"react": "^19.3.1",
|
||||
"react-dom": "^19.3.1",
|
||||
"react-frame-component": "^5.3.2",
|
||||
"react-router": "^8.4.0",
|
||||
"sanitize-filename": "1.6.4",
|
||||
@@ -67,14 +67,14 @@
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^5.3.1",
|
||||
"babel-jest": "^30.5.2",
|
||||
"babel-plugin-transform-import-meta": "^3.0.0",
|
||||
"babel-plugin-transform-import-meta": "^3.1.0",
|
||||
"eslint": "9.7",
|
||||
"eslint-plugin-jest": "^29.15.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.4.0",
|
||||
"jest": "^30.5.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom": "^30.1.2",
|
||||
"jsdom": "^30.1.3",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^17.16.0",
|
||||
@@ -106,9 +106,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.4.tgz",
|
||||
"integrity": "sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==",
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-10.0.3.tgz",
|
||||
"integrity": "sha512-3+777rEPsOOjoy5/sRmIzHnoRDaLpm3XWY2dmWn/N7f/XwDG0bcpnFaKL3X9FYJUbNwLmpcJC1bUYexJRxsFlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5653,9 +5653,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-transform-import-meta": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-3.0.0.tgz",
|
||||
"integrity": "sha512-zFtfF6Fx8QMDlPrmuIvgxXGSPG5h+M/T6NUUy4lbnrKptoK+PH0zTEa2PgaGseLTJpBmZ+iLOys7c7NidYmvPQ==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-3.1.0.tgz",
|
||||
"integrity": "sha512-2OecCfO/6alqJf+ZISiJclv9n5ujX7DG05wa6iohXRn4pcuL2+sIFITXBWPTAj+Gc3uWcQh4Yf9vO1P1RPD/jQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -6375,9 +6375,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
|
||||
"integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -7531,15 +7531,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz",
|
||||
"integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"body-parser": "^2.3.0",
|
||||
"content-disposition": "^2.0.1",
|
||||
"content-type": "^2.0.0",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
@@ -7555,14 +7555,14 @@
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"proxy-addr": "^2.0.8",
|
||||
"qs": "^6.16.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"type-is": "^2.1.0",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -7590,15 +7590,6 @@
|
||||
"serve-static": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
@@ -10867,9 +10858,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz",
|
||||
"integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz",
|
||||
"integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -10882,21 +10873,37 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
"argparse": "^3.0.3"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml/node_modules/argparse": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz",
|
||||
"integrity": "sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "PSF-2.0"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "30.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.2.tgz",
|
||||
"integrity": "sha512-0FFE/jE1rppmVfUrJUgxqXjcwolZYIGtAgj1pTussMhNZxIxi7W/PvfWaMNroGi2B36X1IKHbexpZ9DhkoOPiQ==",
|
||||
"version": "30.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.3.tgz",
|
||||
"integrity": "sha512-hQMUPvRgN7KxsfIytDQji7bimuJeRQa6Qo5+iuiZQk5CafeRxLIA10/cG5YCBFNpD9RO3oqIiN4CO1Y44kxqXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^7.1.3",
|
||||
"@asamuzakjp/dom-selector": "^9.2.4",
|
||||
"@asamuzakjp/dom-selector": "^10.0.3",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.15",
|
||||
"@exodus/bytes": "^1.16.0",
|
||||
@@ -10909,7 +10916,7 @@
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"tough-cookie": "^6.0.2",
|
||||
"undici": "^8.11.2",
|
||||
"undici": "^8.12.1",
|
||||
"w3c-xmlserializer": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
@@ -11054,9 +11061,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/kareem": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.4.0.tgz",
|
||||
"integrity": "sha512-JAKhnR4S027v/8949QXNInRskgETCg9rFUMi1VjhGbjX+XltCAAEFcXykkSocNkom1kc66EMJfjWvhj87BDQKg==",
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.4.1.tgz",
|
||||
"integrity": "sha512-UlgCAFNAdk/HLSsbfkmbH8pJqpEN52Ek8RG53Ys1i3Q3gbCQruSrWpplZlgIGyDHX/LNoLW97ei3OG43s7UhUA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -11531,9 +11538,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked-emoji": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-2.0.3.tgz",
|
||||
"integrity": "sha512-fChW/AfUqCHgoEC1nFDgiw3OR/qsi71/QXH/HTo05yd6B5+T+VHh1SqCpn/HpeGLDxkA+MK4+hr4eULB2/A8Jw==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-emoji/-/marked-emoji-3.0.0.tgz",
|
||||
"integrity": "sha512-4IODef6jiQfuRsWOXSH/q9YiXmpVemvm1raj4WmObkWeAPg5bMMPTvbDpxbn4BIvbM245a1Dhq6RAzCS0WFGOw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"marked": ">=4 <19"
|
||||
@@ -11561,25 +11568,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked-hbfm": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/marked-hbfm/-/marked-hbfm-1.0.1.tgz",
|
||||
"integrity": "sha512-mqz1ALIk0POeRCGjAGDTRhyKrMe+i5ycsxLV/8MlG13xC/oTEXA66VHZvIMNLotdM8jNNnLC9TFLCWECN4QHug==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/marked-hbfm/-/marked-hbfm-1.0.3.tgz",
|
||||
"integrity": "sha512-j+TLQqHv1PGb/ypxh8r+kGlgto1t3373GxxDn1gTBk473QC85uV4PteZfLLq8U5H2OLODbbyEGzxVvYdnOT0lQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eslint-plugin-jest": "^29.16.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"lodash": "^4.18.1",
|
||||
"marked": ">=3 <19",
|
||||
"marked-alignment-paragraphs": "^1.0.0",
|
||||
"marked-definition-lists": "^1.0.1",
|
||||
"marked-diagrams-markdeep": "^1.0.1",
|
||||
"marked-emoji": "^2.0.3",
|
||||
"marked-extended-tables": "^2.0.1",
|
||||
"marked-gfm-heading-id": "^4.1.4",
|
||||
"marked-nonbreaking-spaces": "^1.0.1",
|
||||
"marked-smartypants-lite": "^1.0.3",
|
||||
"marked-subsuper-text": "^1.0.4",
|
||||
"marked-variables": "^1.0.7"
|
||||
"marked-alignment-paragraphs": "1.0.0",
|
||||
"marked-definition-lists": "1.0.1",
|
||||
"marked-diagrams-markdeep": "1.0.1",
|
||||
"marked-emoji": "3.0.0",
|
||||
"marked-extended-tables": "2.0.1",
|
||||
"marked-gfm-heading-id": "4.1.4",
|
||||
"marked-nonbreaking-spaces": "1.0.1",
|
||||
"marked-smartypants-lite": "1.0.3",
|
||||
"marked-subsuper-text": "1.0.4",
|
||||
"marked-variables": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/marked-nonbreaking-spaces": {
|
||||
@@ -11610,9 +11617,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked-variables": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-1.0.7.tgz",
|
||||
"integrity": "sha512-b7ZxKQJ9TEF/xi7dpP//IyIvK10minaYqmQNX7K91K9189jSQ8grpxY9yOIAXpPmVfReY0aZbfxugolTBIpXAA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-variables/-/marked-variables-2.0.0.tgz",
|
||||
"integrity": "sha512-U3LQ7mXfXpM0H2jExenENOvuFrP4JzSksRUr0yyENxZEkK3jmSlUEdT9LBuYxjOqWlMMhqiFVPOmtm1/QJnS+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"expr-eval": "^2.0.2",
|
||||
@@ -11886,13 +11893,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "9.11.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.11.1.tgz",
|
||||
"integrity": "sha512-fVtjTVACOtAb0PjDDF+O5NebcEJy5JpAH+AXk16/5i36w38YkW6MZ+KWV54bdIOJFymDz1VjjA5Xg6L2QfjQjQ==",
|
||||
"version": "9.11.2",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.11.2.tgz",
|
||||
"integrity": "sha512-hFkl/2dccpD58coRAya3a/ht8ooAx9YPLzBfxVX9bEsgcrjsNkvuWQe8gplGfDdmzpw2RfFX1IB/G8PiFwyvwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.1.0",
|
||||
"kareem": "3.4.0",
|
||||
"kareem": "3.4.1",
|
||||
"mongodb": "~7.7",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "6.0.0",
|
||||
@@ -12131,9 +12138,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node": {
|
||||
"version": "26.11.1",
|
||||
"resolved": "https://registry.npmjs.org/node/-/node-26.11.1.tgz",
|
||||
"integrity": "sha512-KimXTKFoiWoakmW/7c2B3EFwZYrAjTjllg7kKSIQAQ7Ai+gRkkIoCrtrURJDrql+6hLshRWvZUmhkbE+JfKkFw==",
|
||||
"version": "26.12.0",
|
||||
"resolved": "https://registry.npmjs.org/node/-/node-26.12.0.tgz",
|
||||
"integrity": "sha512-/P4rDRs4lOBFW0UUDb1Yuy02aDd9oh/RHZfl3haBnatqUcDnY0np4fo9EgK7zhX09bzZ/uroJfyzW+Tgo5kyiA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -12833,9 +12840,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.9",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz",
|
||||
"integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==",
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
|
||||
"integrity": "sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
@@ -13085,24 +13092,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
|
||||
"version": "19.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.3.1.tgz",
|
||||
"integrity": "sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
|
||||
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
|
||||
"version": "19.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz",
|
||||
"integrity": "sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.3.0"
|
||||
"react": "^19.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-frame-component": {
|
||||
@@ -14887,9 +14894,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz",
|
||||
"integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==",
|
||||
"version": "8.12.2",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
|
||||
"integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
+12
-11
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.24.1",
|
||||
"version": "3.24.2",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"npm": ">=10.8 <12",
|
||||
@@ -26,6 +26,7 @@
|
||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
||||
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
||||
"test:content-negotiation": "jest \"server/middleware/.*.spec.js\" --verbose",
|
||||
"test:static-content": "jest \"tests/routes/static-content.test.js\" --verbose",
|
||||
"test:coverage": "jest --coverage --silent --runInBand",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
@@ -115,27 +116,27 @@
|
||||
"cors": "^2.8.5",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent": "^1.7.2",
|
||||
"express": "^5.1.0",
|
||||
"express": "^5.3.0",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "3.0.2",
|
||||
"fflate": "^0.8.3",
|
||||
"fs-extra": "^11.4.1",
|
||||
"hash-wasm": "^4.12.0",
|
||||
"idb-keyval": "^6.2.5",
|
||||
"js-yaml": "^5.4.3",
|
||||
"js-yaml": "^5.5.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^4.9.1",
|
||||
"lodash": "^4.18.1",
|
||||
"marked-hbfm": "^1.0.1",
|
||||
"marked-hbfm": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.31.0",
|
||||
"mongoose": "^9.11.1",
|
||||
"mongoose": "^9.11.2",
|
||||
"nanoid": "6.0.2",
|
||||
"nconf": "^0.13.0",
|
||||
"node": "^26.11.1",
|
||||
"prettier": "^3.9.9",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"node": "^26.12.0",
|
||||
"prettier": "^3.9.10",
|
||||
"react": "^19.3.1",
|
||||
"react-dom": "^19.3.1",
|
||||
"react-frame-component": "^5.3.2",
|
||||
"react-router": "^8.4.0",
|
||||
"sanitize-filename": "1.6.4",
|
||||
@@ -144,14 +145,14 @@
|
||||
"devDependencies": {
|
||||
"@stylistic/stylelint-plugin": "^5.3.1",
|
||||
"babel-jest": "^30.5.2",
|
||||
"babel-plugin-transform-import-meta": "^3.0.0",
|
||||
"babel-plugin-transform-import-meta": "^3.1.0",
|
||||
"eslint": "9.7",
|
||||
"eslint-plugin-jest": "^29.15.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.4.0",
|
||||
"jest": "^30.5.2",
|
||||
"jest-expect-message": "^1.1.3",
|
||||
"jsdom": "^30.1.2",
|
||||
"jsdom": "^30.1.3",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"postcss-less": "^6.0.0",
|
||||
"stylelint": "^17.16.0",
|
||||
|
||||
+32
-67
@@ -26,17 +26,8 @@ const isStaticTheme = (renderer, themeName)=>{
|
||||
return Themes[renderer]?.[themeName] !== undefined;
|
||||
};
|
||||
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
// });
|
||||
// };
|
||||
|
||||
|
||||
const migrateSystemsToTags = (brew)=>{
|
||||
if(!('systems' in brew)) return brew;
|
||||
|
||||
if(!Array.isArray(brew.systems) || brew.systems.length === 0) {
|
||||
if(!('systems' in brew) || !Array.isArray(brew.systems) || brew.systems.length === 0) {
|
||||
brew.systems = undefined;
|
||||
return brew;
|
||||
}
|
||||
@@ -107,19 +98,17 @@ const api = {
|
||||
|
||||
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
||||
|
||||
if(brews) {
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
for (const brew of brews) {
|
||||
userThemes[brew.renderer] ??= {};
|
||||
userThemes[brew.renderer][brew.shareId] = {
|
||||
name : brew.title,
|
||||
renderer : brew.renderer,
|
||||
baseTheme : brew.theme,
|
||||
baseSnippets : false,
|
||||
author : brew.authors[0],
|
||||
path : brew.shareId,
|
||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
||||
};
|
||||
}
|
||||
|
||||
return userThemes;
|
||||
@@ -188,18 +177,14 @@ const api = {
|
||||
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||
|
||||
|
||||
|
||||
const fixedStub = migrateSystemsToTags(stub);
|
||||
req.brew = fixedStub;
|
||||
req.brew = migrateSystemsToTags(stub);
|
||||
splitTextStyleAndMetadata(req.brew);
|
||||
next();
|
||||
};
|
||||
},
|
||||
getCSS : async (req, res)=>{
|
||||
const { brew } = req;
|
||||
if(!brew) return res.status(404).send('');
|
||||
if(!brew.style) return res.status(404).send('');
|
||||
if(!brew?.style) return res.status(404).send('');
|
||||
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
@@ -215,16 +200,12 @@ const api = {
|
||||
`\`\`\`\n\n` +
|
||||
`${text}`;
|
||||
}
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme']);
|
||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'renderer', 'theme', 'columns', 'columnGutter', 'license', 'legalAuthors']);
|
||||
const snippetsArray = brewSnippetsToJSON('brew_snippets', brew.snippets, null, false).snippets;
|
||||
metadata.snippets = snippetsArray.length > 0 ? snippetsArray : undefined;
|
||||
metadata.bleedSize = { top: brew?.bleedSize?.top, bottom: brew?.bleedSize?.bottom, inner: brew?.bleedSize?.inner, outer: brew?.bleedSize?.outer };
|
||||
metadata.safetySpace = { top: brew?.safetySpace?.top, bottom: brew?.safetySpace?.bottom, outer: brew?.safetySpace?.outer, inner: brew?.safetySpace?.inner };
|
||||
metadata.trimSize = { width: brew?.trimSize?.width, height: brew?.trimSize?.height };
|
||||
metadata.columns = brew?.columns;
|
||||
metadata.columnGutter = brew?.columnGutter;
|
||||
metadata.license = brew?.license;
|
||||
metadata.legalAuthors = brew?.legalAuthors;
|
||||
|
||||
text = `\`\`\`metadata\n` +
|
||||
`${yaml.dump(metadata)}\n` +
|
||||
@@ -237,28 +218,14 @@ const api = {
|
||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||
.slice(0, MAX_TITLE_LENGTH);
|
||||
},
|
||||
excludePropsFromUpdate : (brew)=>{
|
||||
// Remove undesired properties
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['_id', 'views', 'lastViewed'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
excludePropsFromUpdate : (brew)=>{ //Remove props that should not update/overwrite when editing
|
||||
return _.omit(brew, ['_id', 'views', 'lastViewed']);
|
||||
},
|
||||
excludeGoogleProps : (brew)=>{
|
||||
const modified = _.clone(brew);
|
||||
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete modified[prop];
|
||||
}
|
||||
return modified;
|
||||
excludeGoogleProps : (brew)=>{ //Remove props that should not be saved to Google (only stored to Homebrewery stub)
|
||||
return _.omit(brew, ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail']);
|
||||
},
|
||||
excludeStubProps : (brew)=>{
|
||||
const propsToExclude = ['text', 'textBin'];
|
||||
for (const prop of propsToExclude) {
|
||||
brew[prop] = undefined;
|
||||
}
|
||||
excludeStubProps : (brew)=>{ //Remove props that should not be saved to stub-only brews
|
||||
brew.text = brew.textBin = undefined; // Can't use _.omit because this brew is a Mongoose object
|
||||
return brew;
|
||||
},
|
||||
beforeNewSave : (account, brew)=>{
|
||||
@@ -277,9 +244,7 @@ const api = {
|
||||
newGoogleBrew : async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
|
||||
const newBrew = api.excludeGoogleProps(brew);
|
||||
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, api.excludeGoogleProps(brew));
|
||||
},
|
||||
newBrew : async (req, res)=>{
|
||||
const brew = req.body;
|
||||
@@ -291,7 +256,7 @@ const api = {
|
||||
|
||||
api.beforeNewSave(req.account, brew);
|
||||
|
||||
const newHomebrew = new HomebrewModel(brew);
|
||||
let newHomebrew = new HomebrewModel(brew);
|
||||
newHomebrew.editId = nanoid(12);
|
||||
newHomebrew.shareId = nanoid(12);
|
||||
|
||||
@@ -300,7 +265,7 @@ const api = {
|
||||
googleId = await api.newGoogleBrew(req.account, newHomebrew, res);
|
||||
|
||||
if(!googleId) return;
|
||||
api.excludeStubProps(newHomebrew);
|
||||
newHomebrew = api.excludeStubProps(newHomebrew);
|
||||
newHomebrew.googleId = googleId;
|
||||
}
|
||||
|
||||
@@ -400,16 +365,17 @@ const api = {
|
||||
const patches = parsePatch(brewFromClient.patches);
|
||||
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
|
||||
result = applyPatches(patches, encodeURI(brewFromServer.text));
|
||||
const failedPatches = patches.map((patch, index)=>{if(!result[1][index]){ return patch; }});
|
||||
if(failedPatches > 0){
|
||||
throw (`Patch failure: ${failedPatches}/${result[1].length} did not apply`);
|
||||
const failedPatchCount = result[1].filter((patchApplied)=>!patchApplied).length; //Count how many patches failed to apply
|
||||
if(failedPatchCount){
|
||||
throw (`Patch failure: ${failedPatchCount}/${result[1].length} did not apply`);
|
||||
}
|
||||
if(decodeURI(result[0]) != brewFromClient.text){
|
||||
patchedText = decodeURI(result[0]);
|
||||
if(patchedText != brewFromClient.text){
|
||||
debugTextMismatch(brewFromClient.text, patchedText, `edit/${brewFromClient.editId}`);
|
||||
throw ('Patches did not apply cleanly, text mismatch detected');
|
||||
}
|
||||
// brew.text = applyPatches(patches, brewFromServer.text)[0];
|
||||
} catch (err) {
|
||||
debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
|
||||
console.error('Failed to apply patches:', {
|
||||
// patches : brewFromClient.patches,
|
||||
// result : result,
|
||||
@@ -420,10 +386,9 @@ const api = {
|
||||
// throw err; // rethrow to preserve the 500 behavior
|
||||
}
|
||||
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
|
||||
migrateSystemsToTags(brew);
|
||||
let brew = _.assign(brewFromServer, brewFromClient);
|
||||
|
||||
brew = migrateSystemsToTags(brew);
|
||||
brew.title = brew.title.trim();
|
||||
brew.description = brew.description.trim() || '';
|
||||
brew.text = api.mergeBrewText(brew);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import supertest from 'supertest';
|
||||
import createApp from '../../server/app.js';
|
||||
|
||||
let app;
|
||||
let request;
|
||||
|
||||
beforeAll(async ()=>{
|
||||
app = await createApp();
|
||||
request = supertest.agent(app).set('X-Forwarded-Proto', 'https');
|
||||
});
|
||||
|
||||
describe('Tests for static assets and cache settings', ()=>{
|
||||
it('Test regular CSS file', async ()=>{
|
||||
const results = await request.get('/fonts/fonts/fonts.css');
|
||||
expect(results.header['content-type']).toBe('text/css; charset=utf-8');
|
||||
expect(results.header['cache-control']).toBe('public, max-age: 43200, must-revalidate');
|
||||
});
|
||||
|
||||
it('Test regular font file', async ()=>{
|
||||
const results = await request.get('/fonts/fonts/open-sans-latin-400-normal.woff2');
|
||||
expect(results.header['content-type']).toBe('font/woff2');
|
||||
expect(results.header['cache-control']).toBe('public, max-age=2592000, must-revalidate');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user