+ return (
+
Properties Editor
- this.handleFieldChange('title', e)} />
+ handleFieldChange('title', 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.'
/>
-
- {this.renderLanguageDropdown()}
-
- {this.renderThemeDropdown()}
-
- {this.renderRenderOptions()}
+ {renderLanguageDropdown()}
+ {renderThemeDropdown()}
+ {renderRenderOptions()}
Authors
-
- {this.renderAuthors()}
-
+ {renderAuthors()}
@@ -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)}
/>
Privacy
-
- {this.renderPublish()}
-
Published brews are searchable in the Vault 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.
+ {renderPublish()}
+
+ Published brews are searchable in the Vault 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.
+
-
- {this.renderDelete()}
-
-
;
- }
-});
+ {metadata.editId &&
+
+
+
+
+
}
+
+ );
+};
export default MetadataEditor;
diff --git a/client/homebrew/pages/basePages/uiPage/uiPage.jsx b/client/homebrew/pages/basePages/uiPage/uiPage.jsx
index 400dbb28e..9fa7e08bb 100644
--- a/client/homebrew/pages/basePages/uiPage/uiPage.jsx
+++ b/client/homebrew/pages/basePages/uiPage/uiPage.jsx
@@ -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 })=>
+
+
+ {brew?.title}
+
+
+
+
+
+
+
+
-const UIPage = createReactClass({
- displayName : 'UIPage',
-
- render : function(){
- return
-
-
- {this.props.brew.title}
-
-
-
-
-
-
-
-
-
-
-
- {this.props.children}
-
-
;
- }
-});
+
+ {children}
+
+
;
export default UIPage;
diff --git a/client/homebrew/pages/vaultPage/vaultPage.less b/client/homebrew/pages/vaultPage/vaultPage.less
index 85eabe7a0..72ff916fe 100644
--- a/client/homebrew/pages/vaultPage/vaultPage.less
+++ b/client/homebrew/pages/vaultPage/vaultPage.less
@@ -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 {
diff --git a/package-lock.json b/package-lock.json
index 0e4277d3a..8374fb57a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
diff --git a/package.json b/package.json
index 6f1269f97..7f2f4fae6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/server/homebrew.api.js b/server/homebrew.api.js
index efea261b3..3d80ed5a6 100644
--- a/server/homebrew.api.js
+++ b/server/homebrew.api.js
@@ -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);
diff --git a/tests/routes/static-content.test.js b/tests/routes/static-content.test.js
new file mode 100644
index 000000000..3d307ca00
--- /dev/null
+++ b/tests/routes/static-content.test.js
@@ -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');
+ });
+});