From 89a788ff9f773ca215de4da14866c64bfdd9601e Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 3 Sep 2023 16:56:21 +1200 Subject: [PATCH 01/48] Add new theme - Unearthed Arcana --- themes/V3/UnearthedArcana/dropdownPreview.png | Bin 0 -> 140438 bytes themes/V3/UnearthedArcana/dropdownTexture.png | Bin 0 -> 236 bytes themes/V3/UnearthedArcana/settings.json | 6 +++ themes/V3/UnearthedArcana/style.less | 38 ++++++++++++++++++ themes/themes.json | 7 ++++ 5 files changed, 51 insertions(+) create mode 100644 themes/V3/UnearthedArcana/dropdownPreview.png create mode 100644 themes/V3/UnearthedArcana/dropdownTexture.png create mode 100644 themes/V3/UnearthedArcana/settings.json create mode 100644 themes/V3/UnearthedArcana/style.less diff --git a/themes/V3/UnearthedArcana/dropdownPreview.png b/themes/V3/UnearthedArcana/dropdownPreview.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc1e36bc5dba7580c10c02d42dc6b92b9671097 GIT binary patch literal 140438 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 literal 0 HcmV?d00001 diff --git a/themes/V3/UnearthedArcana/settings.json b/themes/V3/UnearthedArcana/settings.json new file mode 100644 index 000000000..273f0bb2f --- /dev/null +++ b/themes/V3/UnearthedArcana/settings.json @@ -0,0 +1,6 @@ +{ + "name" : "UnearthedArcana", + "renderer" : "V3", + "baseTheme" : false, + "baseSnippets" : false +} diff --git a/themes/V3/UnearthedArcana/style.less b/themes/V3/UnearthedArcana/style.less new file mode 100644 index 000000000..695924d37 --- /dev/null +++ b/themes/V3/UnearthedArcana/style.less @@ -0,0 +1,38 @@ +@import (less) './themes/fonts/5e/fonts.less'; +@import (less) './themes/assets/assets.less'; + +:root { + //Colors + --HB_Color_Background : #FFFFFF; // White + --HB_Color_WatercolorStain : #000000; // Black +} + +.page { + font-family: Cambria,Georgia,serif; + font-size: 14px; + h1, h2, h3, h4 { + font-variant: small-caps; + font-weight: normal; + } + h1 { + column-span: all; + -webkit-column-span: all; + font-size: 40px; + } + h2 { + font-size: 26px; + } + h3 { + font-size: 20px; + border-bottom: 2px solid black; + } + h4 { + font-size: 18px; + } + h5 { + font-size: 16px; + } + h6 { + font-size: 14px; + } +} diff --git a/themes/themes.json b/themes/themes.json index 0d28c7394..c3a4a3b75 100644 --- a/themes/themes.json +++ b/themes/themes.json @@ -35,6 +35,13 @@ "baseTheme": false, "baseSnippets": "5ePHB", "path": "Journal" + }, + "UnearthedArcana": { + "name": "UnearthedArcana", + "renderer": "V3", + "baseTheme": false, + "baseSnippets": false, + "path": "UnearthedArcana" } } } \ No newline at end of file From e842599b2229077ec9afebf47c54c0a4a3102194 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Sat, 24 May 2025 22:35:03 -0500 Subject: [PATCH 02/48] Add missing punction and sentence structure characters to mustache style assignment regex --- client/homebrew/editor/editor.jsx | 2 +- shared/naturalcrit/markdown.js | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index cf9a17303..7faa22f16 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -12,7 +12,7 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME'; -const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; +const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n]*})?$)/m; const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const SNIPPETBAR_HEIGHT = 25; const DEFAULT_STYLE_TEXT = dedent` diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index ac6988734..42bc5b054 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -185,7 +185,7 @@ const mustacheSpans = { start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token - const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; + const inlineRegex = /{{(?=((?:[:=](?:"'['\w,\-()#%=?. \&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\=]*'"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const match = completeSpan.exec(src); if(match) { //Find closing delimiter @@ -242,7 +242,7 @@ const mustacheDivs = { start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token - const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; + const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%. ]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const match = completeBlock.exec(src); if(match) { //Find closing delimiter @@ -297,7 +297,7 @@ const mustacheInjectInline = { level : 'inline', start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -343,7 +343,7 @@ const mustacheInjectBlock = { level : 'block', start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?. ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?.& ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -735,6 +735,16 @@ const voidTags = new Set([ 'input', 'keygen', 'link', 'meta', 'param', 'source' ]); +const notInside = (string, stringMatch1, stringMatch2)=> { + const pos1 = string.indexOf(stringMatch1); + const pos2 = string.indexOf(stringMatch2); + + if((pos1 > 0) && (pos2 > 0) && (pos2 > pos1)) { + return true; + } + return false; +}; + const processStyleTags = (string)=>{ //split tags up. quotes can only occur right after : or =. //TODO: can we simplify to just split on commas? @@ -742,7 +752,7 @@ const processStyleTags = (string)=>{ const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0] || null; const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))).join(' ') || null; - const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')) + const attributes = _.remove(tags, (tag)=>(notInside(tag, '=', ':'))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')) ?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) .reduce((obj, attr)=>{ const index = attr.indexOf('='); From 50d2a0d3a2a842bede5f22feddad7cac1d113485 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Mon, 26 May 2025 23:13:21 -0500 Subject: [PATCH 03/48] FIx regression --- shared/naturalcrit/markdown.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 42bc5b054..ff61e9793 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -185,9 +185,10 @@ const mustacheSpans = { start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token - const inlineRegex = /{{(?=((?:[:=](?:"'['\w,\-()#%=?. \&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\=]*'"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; + const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. \&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\=]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const match = completeSpan.exec(src); if(match) { + console.log('MustacheSpans'); //Find closing delimiter let blockCount = 0; let tags = {}; @@ -195,6 +196,7 @@ const mustacheSpans = { let endToken = 0; let delim; while (delim = inlineRegex.exec(match[0])) { + console.log(delim); if(_.isEmpty(tags)) { tags = processStyleTags(delim[0].substring(2)); endTags = delim[0].length; @@ -245,6 +247,7 @@ const mustacheDivs = { const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%. ]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const match = completeBlock.exec(src); if(match) { + console.log('Mustache Divs'); //Find closing delimiter let blockCount = 0; let tags = {}; @@ -300,6 +303,7 @@ const mustacheInjectInline = { const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const match = inlineRegex.exec(src); if(match) { + console.log('InlineInject'); const lastToken = tokens[tokens.length - 1]; if(!lastToken || lastToken.type == 'mustacheInjectInline') return false; @@ -739,7 +743,8 @@ const notInside = (string, stringMatch1, stringMatch2)=> { const pos1 = string.indexOf(stringMatch1); const pos2 = string.indexOf(stringMatch2); - if((pos1 > 0) && (pos2 > 0) && (pos2 > pos1)) { + if(((pos1 > 0) && (pos2 == -1)) || + ((pos1 > 0) && (pos2 > 0) && (pos2 > pos1))) { return true; } return false; From 16d7b11b8d03037fbc2db3c4f6ffcc7d62848242 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sat, 26 Jul 2025 18:44:59 +1200 Subject: [PATCH 04/48] Add request-middleware test file --- .../homebrew/utils/request-middleware.spec.js | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 client/homebrew/utils/request-middleware.spec.js diff --git a/client/homebrew/utils/request-middleware.spec.js b/client/homebrew/utils/request-middleware.spec.js new file mode 100644 index 000000000..d7c198394 --- /dev/null +++ b/client/homebrew/utils/request-middleware.spec.js @@ -0,0 +1,74 @@ +import requestMiddleware from './request-middleware'; + +jest.mock('superagent'); +import request from 'superagent'; + +describe('request-middleware', ()=>{ + let version; + + let setFn; + let testFn; + + beforeEach(()=>{ + jest.resetAllMocks(); + version = global.version; + + global.version = '999'; + + setFn = jest.fn(); + testFn = jest.fn(()=>{ return { set: setFn }; }); + }); + + afterEach(()=>{ + global.version = version; + }); + + it('should add header to get', ()=>{ + // Ensure tests functions have been reset + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.get = testFn; + + requestMiddleware.get('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); + + it('should add header to put', ()=>{ + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.put = testFn; + + requestMiddleware.put('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); + + it('should add header to post', ()=>{ + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.post = testFn; + + requestMiddleware.post('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); + + it('should add header to delete', ()=>{ + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.delete = testFn; + + requestMiddleware.delete('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); +}); From 719edd82c5d2b3459f7097621e6fb79db481c15b Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 29 Jul 2025 16:35:25 -0400 Subject: [PATCH 05/48] Moving splitPane over to the components folder Just to reduce the number of changes needed to review on the UI overhaul #4122 PR --- .../naturalcrit => client/components}/splitPane/splitPane.jsx | 0 .../naturalcrit => client/components}/splitPane/splitPane.less | 0 client/homebrew/pages/editPage/editPage.jsx | 2 +- client/homebrew/pages/homePage/homePage.jsx | 2 +- client/homebrew/pages/newPage/newPage.jsx | 2 +- client/homebrew/pages/vaultPage/vaultPage.jsx | 2 +- 6 files changed, 4 insertions(+), 4 deletions(-) rename {shared/naturalcrit => client/components}/splitPane/splitPane.jsx (100%) rename {shared/naturalcrit => client/components}/splitPane/splitPane.less (100%) diff --git a/shared/naturalcrit/splitPane/splitPane.jsx b/client/components/splitPane/splitPane.jsx similarity index 100% rename from shared/naturalcrit/splitPane/splitPane.jsx rename to client/components/splitPane/splitPane.jsx diff --git a/shared/naturalcrit/splitPane/splitPane.less b/client/components/splitPane/splitPane.less similarity index 100% rename from shared/naturalcrit/splitPane/splitPane.less rename to client/components/splitPane/splitPane.less diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 29dad9de0..d1f0ed21c 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -21,7 +21,7 @@ const Account = require('../../navbar/account.navitem.jsx'); const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const VaultNavItem = require('../../navbar/vault.navitem.jsx'); -const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); +const SplitPane = require('client/components/splitPane/splitPane.jsx'); const Editor = require('../../editor/editor.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index d03e30c91..eac0216fd 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -15,7 +15,7 @@ const AccountNavItem = require('../../navbar/account.navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const { fetchThemeBundle } = require('../../../../shared/helpers.js'); -const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); +const SplitPane = require('client/components/splitPane/splitPane.jsx'); const Editor = require('../../editor/editor.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 64fac86c0..ab7c22541 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -14,7 +14,7 @@ const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const HelpNavItem = require('../../navbar/help.navitem.jsx'); -const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); +const SplitPane = require('client/components/splitPane/splitPane.jsx'); const Editor = require('../../editor/editor.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); diff --git a/client/homebrew/pages/vaultPage/vaultPage.jsx b/client/homebrew/pages/vaultPage/vaultPage.jsx index f979aa4f7..e098bb1a2 100644 --- a/client/homebrew/pages/vaultPage/vaultPage.jsx +++ b/client/homebrew/pages/vaultPage/vaultPage.jsx @@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx'); const NewBrew = require('../../navbar/newbrew.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx'); const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx'); -const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx'); +const SplitPane = require('client/components/splitPane/splitPane.jsx'); const ErrorIndex = require('../errorPage/errors/errorIndex.js'); import request from '../../utils/request-middleware.js'; From a8dab28fcf376b77ca0382e2c174e149e8aa6ed6 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Wed, 30 Jul 2025 12:00:50 +1200 Subject: [PATCH 06/48] Fix Account page FA icon font weights --- client/homebrew/pages/basePages/uiPage/uiPage.less | 1 + 1 file changed, 1 insertion(+) diff --git a/client/homebrew/pages/basePages/uiPage/uiPage.less b/client/homebrew/pages/basePages/uiPage/uiPage.less index 39ccf1d74..f00b484c0 100644 --- a/client/homebrew/pages/basePages/uiPage/uiPage.less +++ b/client/homebrew/pages/basePages/uiPage/uiPage.less @@ -29,6 +29,7 @@ &::before { margin-right : 5px; font-family : 'Font Awesome 6 Free'; + font-weight : 900; content : '\f00c'; } } From 1eb226ea13fe31f80e1e11a5eed44f6fd4bb0727 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Sun, 10 Aug 2025 23:26:41 -0700 Subject: [PATCH 07/48] Handle mongo count qurey error by returning default value Signed-off-by: Emmanuel Ferdman --- server/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app.js b/server/app.js index 869fe6555..afba0997b 100644 --- a/server/app.js +++ b/server/app.js @@ -487,8 +487,8 @@ app.get('/account', asyncHandler(async (req, res, next)=>{ const query = { authors: req.account.username, googleId: { $exists: false } }; const mongoCount = await HomebrewModel.countDocuments(query) .catch((err)=>{ - mongoCount = 0; console.log(err); + return 0; }); data.accountDetails = { From aebc49c2d4c74fc761f966f73d4100fff91fa929 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:52:36 +0000 Subject: [PATCH 08/48] Bump the dev-dependencies group across 1 directory with 3 updates Bumps the dev-dependencies group with 3 updates in the / directory: [eslint](https://github.com/eslint/eslint), [stylelint](https://github.com/stylelint/stylelint) and [stylelint-config-recess-order](https://github.com/stormwarning/stylelint-config-recess-order). Updates `eslint` from 9.31.0 to 9.33.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.31.0...v9.33.0) Updates `stylelint` from 16.22.0 to 16.23.1 - [Release notes](https://github.com/stylelint/stylelint/releases) - [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint/compare/16.22.0...16.23.1) Updates `stylelint-config-recess-order` from 7.1.0 to 7.2.0 - [Release notes](https://github.com/stormwarning/stylelint-config-recess-order/releases) - [Changelog](https://github.com/stormwarning/stylelint-config-recess-order/blob/main/CHANGELOG.md) - [Commits](https://github.com/stormwarning/stylelint-config-recess-order/compare/v7.1.0...v7.2.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.33.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: stylelint dependency-version: 16.23.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: stylelint-config-recess-order dependency-version: 7.2.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 154 ++++++++++++++++++---------------------------- package.json | 6 +- 2 files changed, 64 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb82b0f40..20e573a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.3", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.31.0", + "eslint": "^9.33.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", @@ -73,8 +73,8 @@ "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.22.0", - "stylelint-config-recess-order": "^7.1.0", + "stylelint": "^16.23.1", + "stylelint-config-recess-order": "^7.2.0", "stylelint-config-recommended": "^16.0.0", "supertest": "^7.1.4" }, @@ -2021,18 +2021,19 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2080,9 +2081,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "dev": true, "license": "MIT", "engines": { @@ -2102,13 +2103,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -2832,37 +2833,11 @@ } }, "node_modules/@keyv/serialize": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", - "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==", "dev": true, - "dependencies": { - "buffer": "^6.0.3" - } - }, - "node_modules/@keyv/serialize/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } + "license": "MIT" }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.0", @@ -4675,22 +4650,24 @@ } }, "node_modules/cacheable": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.1.tgz", - "integrity": "sha512-Fa2BZY0CS9F0PFc/6aVA6tgpOdw+hmv9dkZOlHXII5v5Hw+meJBIWDcPrG9q/dXxGcNbym5t77fzmawrBQfTmQ==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.3.tgz", + "integrity": "sha512-M6p10iJ/VT0wT7TLIGUnm958oVrU2cUK8pQAVU21Zu7h8rbk/PeRtRWrvHJBql97Bhzk3g1N6+2VKC+Rjxna9Q==", "dev": true, + "license": "MIT", "dependencies": { "hookified": "^1.10.0", - "keyv": "^5.3.4" + "keyv": "^5.4.0" } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.4.tgz", - "integrity": "sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.0.tgz", + "integrity": "sha512-QG7qR2tijh1ftOvClut4YKKg1iW6cx3GZsKoGyJPxHkGWK9oJhG9P3j5deP0QQOGDowBMVQFaP+Vm4NpGYvmIQ==", "dev": true, + "license": "MIT", "dependencies": { - "@keyv/serialize": "^1.0.3" + "@keyv/serialize": "^1.1.0" } }, "node_modules/cached-path-relative": { @@ -6080,20 +6057,20 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -6258,19 +6235,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7648,10 +7612,11 @@ } }, "node_modules/hookified": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.10.0.tgz", - "integrity": "sha512-dJw0492Iddsj56U1JsSTm9E/0B/29a1AuoSLRAte8vQg/kaTGF3IgjEWT8c8yG4cC10+HisE1x5QAwR0Xwc+DA==", - "dev": true + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.11.0.tgz", + "integrity": "sha512-aDdIN3GyU5I6wextPplYdfmWCo+aLmjjVbntmX6HLD5RCi/xKsivYEBhnRD+d9224zFf008ZpLMPlWF0ZodYZw==", + "dev": true, + "license": "MIT" }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", @@ -13306,9 +13271,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.22.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.22.0.tgz", - "integrity": "sha512-SVEMTdjKNV4ollUrIY9ordZ36zHv2/PHzPjfPMau370MlL2VYXeLgSNMMiEbLGRO8RmD2R8/BVUeF2DfnfkC0w==", + "version": "16.23.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.23.1.tgz", + "integrity": "sha512-dNvDTsKV1U2YtiUDfe9d2gp902veFeo3ecCWdGlmLm2WFrAV0+L5LoOj/qHSBABQwMsZPJwfC4bf39mQm1S5zw==", "dev": true, "funding": [ { @@ -13335,7 +13300,7 @@ "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.1.1", + "file-entry-cache": "^10.1.3", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", @@ -13369,10 +13334,11 @@ } }, "node_modules/stylelint-config-recess-order": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.1.0.tgz", - "integrity": "sha512-rFc4Z6SCGgEohr1khsmAZ83X56Tdi2dHY/GB7VT3qJkpKU1V2w+mYlK+b7Za5gpsxEng3jnb4FzWyIl/KTH0AQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.2.0.tgz", + "integrity": "sha512-3Y97dhsWkUHFKRLGNLF6LE5JuNB2EVAZKYJ41wBRK4gplYdk7eHhSIwE24hanu0AoNmv5534Djip70pE+y5qkA==", "dev": true, + "license": "ISC", "peerDependencies": { "stylelint": ">=16.18", "stylelint-order": ">=7" @@ -13472,21 +13438,23 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.1.tgz", - "integrity": "sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.3.tgz", + "integrity": "sha512-D+w75Ub8T55yor7fPgN06rkCAUbAYw2vpxJmmjv/GDAcvCnv9g7IvHhIZoxzRZThrXPFI2maeY24pPbtyYU7Lg==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^6.1.10" + "flat-cache": "^6.1.12" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.11.tgz", - "integrity": "sha512-zfOAns94mp7bHG/vCn9Ru2eDCmIxVQ5dELUHKjHfDEOJmHNzE+uGa6208kfkgmtym4a0FFjEuFksCXFacbVhSg==", + "version": "6.1.12", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.12.tgz", + "integrity": "sha512-U+HqqpZPPXP5d24bWuRzjGqVqUcw64k4nZAbruniDwdRg0H10tvN7H6ku1tjhA4rg5B9GS3siEvwO2qjJJ6f8Q==", "dev": true, + "license": "MIT", "dependencies": { - "cacheable": "^1.10.1", + "cacheable": "^1.10.3", "flatted": "^3.3.3", "hookified": "^1.10.0" } diff --git a/package.json b/package.json index e32f5a05c..fd905d209 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^3.1.3", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.31.0", + "eslint": "^9.33.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", @@ -146,8 +146,8 @@ "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.22.0", - "stylelint-config-recess-order": "^7.1.0", + "stylelint": "^16.23.1", + "stylelint-config-recess-order": "^7.2.0", "stylelint-config-recommended": "^16.0.0", "supertest": "^7.1.4" } From cca9ebefdb32d175b39e34e4cb2fa94b3df820d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 20 Aug 2025 23:23:13 +0200 Subject: [PATCH 09/48] better error handling for file import --- client/homebrew/navbar/newbrew.navitem.jsx | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/client/homebrew/navbar/newbrew.navitem.jsx b/client/homebrew/navbar/newbrew.navitem.jsx index 30d53c675..23b7c9e5e 100644 --- a/client/homebrew/navbar/newbrew.navitem.jsx +++ b/client/homebrew/navbar/newbrew.navitem.jsx @@ -5,7 +5,7 @@ const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // const BREWKEY = 'homebrewery-new'; const STYLEKEY = 'homebrewery-new-style'; -const METAKEY = 'homebrewery-new-meta'; +const METAKEY = 'homebrewery-new-meta'; const NewBrew = ()=>{ const handleFileChange = (e)=>{ @@ -22,10 +22,26 @@ const NewBrew = ()=>{ splitTextStyleAndMetadata(newBrew); // Modify newBrew directly localStorage.setItem(BREWKEY, newBrew.text); localStorage.setItem(STYLEKEY, newBrew.style); - localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']))); + localStorage.setItem( + METAKEY, + JSON.stringify( + _.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']) + ) + ); window.location.href = '/new'; } else { - alert('This file is invalid, please, enter a valid file'); + const type = file.name.split('.').pop().toLowercase(); + if(type === 'txt') { + alert( + `This file type is correct, but it is not from the homebrewery or has been tampered with, + please try with a correct file or report this as an issue if you think it is a mistake.` + ); + } else if(!type) { + alert('This file is invalid, please, enter a valid file'); + console.log(file); + } else { + alert(`This is a .${type} file, only '.txt' files are allowed`); + } } }; reader.readAsText(file); From 320f1e120f638e242b30adfb67b314a402c05244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 21 Aug 2025 12:23:41 +0200 Subject: [PATCH 10/48] reordering --- client/homebrew/navbar/newbrew.navitem.jsx | 73 +++++++++++----------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/client/homebrew/navbar/newbrew.navitem.jsx b/client/homebrew/navbar/newbrew.navitem.jsx index 23b7c9e5e..c20ae6b6c 100644 --- a/client/homebrew/navbar/newbrew.navitem.jsx +++ b/client/homebrew/navbar/newbrew.navitem.jsx @@ -10,44 +10,45 @@ const METAKEY = 'homebrewery-new-meta'; const NewBrew = ()=>{ const handleFileChange = (e)=>{ const file = e.target.files[0]; - if(file) { - const reader = new FileReader(); - reader.onload = (e)=>{ - const fileContent = e.target.result; - const newBrew = { - text : fileContent, - style : '' - }; - if(fileContent.startsWith('```metadata')) { - splitTextStyleAndMetadata(newBrew); // Modify newBrew directly - localStorage.setItem(BREWKEY, newBrew.text); - localStorage.setItem(STYLEKEY, newBrew.style); - localStorage.setItem( - METAKEY, - JSON.stringify( - _.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']) - ) - ); - window.location.href = '/new'; - } else { - const type = file.name.split('.').pop().toLowercase(); - if(type === 'txt') { - alert( - `This file type is correct, but it is not from the homebrewery or has been tampered with, - please try with a correct file or report this as an issue if you think it is a mistake.` - ); - } else if(!type) { - alert('This file is invalid, please, enter a valid file'); - console.log(file); - } else { - alert(`This is a .${type} file, only '.txt' files are allowed`); - } - } - }; - reader.readAsText(file); - } + if(!file) return; + + const currentNew = localStorage.getItem(BREWKEY); + if(currentNew && !confirm( + `You have some text in the new brew space, if you load a file that text will be lost, are you sure you want to load the file?` + )) return; + + const reader = new FileReader(); + reader.onload = (e)=>{ + const fileContent = e.target.result; + const newBrew = { text: fileContent, style: '' }; + + if(fileContent.startsWith('```metadata')) { + splitTextStyleAndMetadata(newBrew); + localStorage.setItem(BREWKEY, newBrew.text); + localStorage.setItem(STYLEKEY, newBrew.style); + localStorage.setItem(METAKEY, JSON.stringify( + _.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']) + )); + window.location.href = '/new'; + return; + } + + const type = file.name.split('.').pop().toLowerCase(); + if(type === 'txt') { + alert( + `This file type is correct, but it is not from the homebrewery or has been tampered with, please try with a correct file or report this as an issue if you think it is a mistake.` + ); + } else if(!type) { + alert('This file is invalid, please, enter a valid file'); + console.log(file); + } else { + alert(`This is a .${type} file, only .txt files are allowed`); + } + }; + reader.readAsText(file); }; + return ( Date: Thu, 21 Aug 2025 13:58:23 +0000 Subject: [PATCH 11/48] Bump @stylistic/stylelint-plugin from 3.1.3 to 4.0.0 Bumps [@stylistic/stylelint-plugin](https://github.com/stylelint-stylistic/stylelint-stylistic) from 3.1.3 to 4.0.0. - [Release notes](https://github.com/stylelint-stylistic/stylelint-stylistic/releases) - [Changelog](https://github.com/stylelint-stylistic/stylelint-stylistic/blob/main/CHANGELOG.md) - [Commits](https://github.com/stylelint-stylistic/stylelint-stylistic/compare/v3.1.3...v4.0.0) --- updated-dependencies: - dependency-name: "@stylistic/stylelint-plugin" dependency-version: 4.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 77 +++++++++++++---------------------------------- package.json | 2 +- 2 files changed, 22 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20e573a6b..705f327b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,7 +63,7 @@ "written-number": "^0.11.1" }, "devDependencies": { - "@stylistic/stylelint-plugin": "^3.1.3", + "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", "eslint": "^9.33.0", "eslint-plugin-jest": "^29.0.1", @@ -1884,9 +1884,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", - "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", "dev": true, "funding": [ { @@ -1898,12 +1898,13 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.1", - "@csstools/css-tokenizer": "^3.0.1" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@dmsnell/diff-match-patch": { @@ -2980,17 +2981,17 @@ } }, "node_modules/@stylistic/stylelint-plugin": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.3.tgz", - "integrity": "sha512-85fsmzgsIVmyG3/GFrjuYj6Cz8rAM7IZiPiXCMiSMfoDOC1lOrzrXPDk24WqviAghnPqGpx8b0caK2PuewWGFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-4.0.0.tgz", + "integrity": "sha512-CFwt3K4Y/7bygNCLCQ8Sy4Hzgbhxq3BsNW0FIuYxl17HD3ywptm54ocyeiLVRrk5jtz1Zwks7Xr9eiZt8SWHAw==", "dev": true, + "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.1", - "@csstools/css-tokenizer": "^3.0.1", - "@csstools/media-query-list-parser": "^3.0.1", - "is-plain-object": "^5.0.0", - "postcss": "^8.4.41", - "postcss-selector-parser": "^6.1.2", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "postcss": "^8.5.6", + "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "style-search": "^0.1.0" }, @@ -2998,7 +2999,7 @@ "node": "^18.12 || >=20.9" }, "peerDependencies": { - "stylelint": "^16.8.0" + "stylelint": "^16.22.0" } }, "node_modules/@tybys/wasm-util": { @@ -11538,10 +11539,11 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -13384,29 +13386,6 @@ "stylelint": "^16.18.0" } }, - "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", - "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, "node_modules/stylelint/node_modules/@csstools/selector-specificity": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", @@ -13468,20 +13447,6 @@ "node": ">= 4" } }, - "node_modules/stylelint/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/stylelint/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", diff --git a/package.json b/package.json index fd905d209..5c7ea1032 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "written-number": "^0.11.1" }, "devDependencies": { - "@stylistic/stylelint-plugin": "^3.1.3", + "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", "eslint": "^9.33.0", "eslint-plugin-jest": "^29.0.1", From dd46a059c539bdf9179a02d004ddd6cd478663bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Thu, 21 Aug 2025 16:53:36 +0200 Subject: [PATCH 12/48] aah, i forgot to add the latest commit --- client/homebrew/navbar/newbrew.navitem.jsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/client/homebrew/navbar/newbrew.navitem.jsx b/client/homebrew/navbar/newbrew.navitem.jsx index c20ae6b6c..ccade4e8b 100644 --- a/client/homebrew/navbar/newbrew.navitem.jsx +++ b/client/homebrew/navbar/newbrew.navitem.jsx @@ -34,16 +34,11 @@ const NewBrew = ()=>{ } const type = file.name.split('.').pop().toLowerCase(); - if(type === 'txt') { - alert( - `This file type is correct, but it is not from the homebrewery or has been tampered with, please try with a correct file or report this as an issue if you think it is a mistake.` - ); - } else if(!type) { - alert('This file is invalid, please, enter a valid file'); - console.log(file); - } else { - alert(`This is a .${type} file, only .txt files are allowed`); - } + + alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`); + + + console.log(file); }; reader.readAsText(file); }; From 029c105ff1f8c070874c5c04696009a5ca4fe029 Mon Sep 17 00:00:00 2001 From: David Bolack Date: Sat, 23 Aug 2025 14:04:37 -0500 Subject: [PATCH 13/48] Fix \} in divblocks --- shared/naturalcrit/markdown.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index ff61e9793..c07d879ec 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -188,7 +188,6 @@ const mustacheSpans = { const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. \&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\=]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const match = completeSpan.exec(src); if(match) { - console.log('MustacheSpans'); //Find closing delimiter let blockCount = 0; let tags = {}; @@ -196,7 +195,6 @@ const mustacheSpans = { let endToken = 0; let delim; while (delim = inlineRegex.exec(match[0])) { - console.log(delim); if(_.isEmpty(tags)) { tags = processStyleTags(delim[0].substring(2)); endTags = delim[0].length; @@ -243,11 +241,10 @@ const mustacheDivs = { level : 'block', start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token + const completeBlock = /^ *{{[^\n]* *\n.*\n *}}/s; // Regex for the complete token const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%. ]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const match = completeBlock.exec(src); if(match) { - console.log('Mustache Divs'); //Find closing delimiter let blockCount = 0; let tags = {}; @@ -303,7 +300,6 @@ const mustacheInjectInline = { const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; const match = inlineRegex.exec(src); if(match) { - console.log('InlineInject'); const lastToken = tokens[tokens.length - 1]; if(!lastToken || lastToken.type == 'mustacheInjectInline') return false; From cd378cad0ca0ce3d3cab047f53c60d2829c10a96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:25:55 +0000 Subject: [PATCH 14/48] Bump stylelint-config-recommended from 16.0.0 to 17.0.0 Bumps [stylelint-config-recommended](https://github.com/stylelint/stylelint-config-recommended) from 16.0.0 to 17.0.0. - [Release notes](https://github.com/stylelint/stylelint-config-recommended/releases) - [Changelog](https://github.com/stylelint/stylelint-config-recommended/blob/main/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint-config-recommended/compare/16.0.0...17.0.0) --- updated-dependencies: - dependency-name: stylelint-config-recommended dependency-version: 17.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 705f327b2..b6b958b21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "postcss-less": "^6.0.0", "stylelint": "^16.23.1", "stylelint-config-recess-order": "^7.2.0", - "stylelint-config-recommended": "^16.0.0", + "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" }, "engines": { @@ -13347,9 +13347,9 @@ } }, "node_modules/stylelint-config-recommended": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-16.0.0.tgz", - "integrity": "sha512-4RSmPjQegF34wNcK1e1O3Uz91HN8P1aFdFzio90wNK9mjgAI19u5vsU868cVZboKzCaa5XbpvtTzAAGQAxpcXA==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-17.0.0.tgz", + "integrity": "sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==", "dev": true, "funding": [ { @@ -13366,7 +13366,7 @@ "node": ">=18.12.0" }, "peerDependencies": { - "stylelint": "^16.16.0" + "stylelint": "^16.23.0" } }, "node_modules/stylelint-order": { diff --git a/package.json b/package.json index 5c7ea1032..415ed8a54 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "postcss-less": "^6.0.0", "stylelint": "^16.23.1", "stylelint-config-recess-order": "^7.2.0", - "stylelint-config-recommended": "^16.0.0", + "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" } } From 8924685c26419628ba4088feac8c27d3da07e706 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 16:30:44 +0000 Subject: [PATCH 15/48] Bump cipher-base from 1.0.4 to 1.0.6 Bumps [cipher-base](https://github.com/crypto-browserify/cipher-base) from 1.0.4 to 1.0.6. - [Changelog](https://github.com/browserify/cipher-base/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/cipher-base/compare/v1.0.4...v1.0.6) --- updated-dependencies: - dependency-name: cipher-base dependency-version: 1.0.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6b958b21..9869b7be9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4863,13 +4863,16 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/cjs-module-lexer": { From e25c3daad6d8a6f16134a6029d82c1b6c1ebc044 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:15:31 +0000 Subject: [PATCH 16/48] Bump sha.js from 2.4.11 to 2.4.12 Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12. - [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12) --- updated-dependencies: - dependency-name: sha.js dependency-version: 2.4.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9869b7be9..623f4ecd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12530,16 +12530,23 @@ "license": "ISC" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shasum": { From f1ad1b912414c00c03d16037abddb6ce09101fc5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 04:44:12 +0000 Subject: [PATCH 17/48] Bump eslint from 9.33.0 to 9.34.0 in the dev-dependencies group Bumps the dev-dependencies group with 1 update: [eslint](https://github.com/eslint/eslint). Updates `eslint` from 9.33.0 to 9.34.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.33.0...v9.34.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.34.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 623f4ecd3..430fb6778 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.33.0", + "eslint": "^9.34.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", @@ -2082,9 +2082,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -6061,9 +6061,9 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -6073,7 +6073,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", diff --git a/package.json b/package.json index 415ed8a54..5c6fb4efc 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.33.0", + "eslint": "^9.34.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", From 83c3eacf831c866a70329468a50559f4a24fcd21 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 16:45:47 -0400 Subject: [PATCH 18/48] Change props and state to functional style --- client/homebrew/pages/homePage/homePage.jsx | 34 +++++++++------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index eac0216fd..55d19d72b 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -21,25 +21,19 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); -const HomePage = createClass({ - displayName : 'HomePage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW, - ver : '0.0.0' - }; - }, - getInitialState : function() { - return { - brew : this.props.brew, - welcomeText : this.props.brew.text, - error : undefined, - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - themeBundle : {} - }; - }, +const HomePage =(props)=>{ + const { + brew = DEFAULT_BREW, + ver = '0.0.0' + } = props; + + const [brew , setBrew] = useState(brew); + const [welcomeText , setWelcomeText] = useState(brew.text); + const [error , setError] = useState(undefined); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); + const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); + const [themeBundle , setThemeBundle] = useState({}); editor : React.createRef(null), @@ -136,6 +130,6 @@ const HomePage = createClass({ ; } -}); +}; module.exports = HomePage; From 759dcb583360061c18a867004bf099eda85bacad Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 16:49:54 -0400 Subject: [PATCH 19/48] Change functions to const vars --- client/homebrew/pages/homePage/homePage.jsx | 40 +++++++++++---------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 55d19d72b..1b3ad0608 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -41,7 +41,7 @@ const HomePage =(props)=>{ fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); }, - handleSave : function(){ + const handleSave ()=>{ request.post('/api') .send(this.state.brew) .end((err, res)=>{ @@ -52,29 +52,31 @@ const HomePage =(props)=>{ const brew = res.body; window.location = `/edit/${brew.editId}`; }); - }, - handleSplitMove : function(){ + }; + + const handleSplitMove ()=>{ this.editor.current.update(); - }, + }; - handleEditorViewPageChange : function(pageNumber){ + const handleEditorViewPageChange (pageNumber)=>{ this.setState({ currentEditorViewPageNum: pageNumber }); - }, + }; - handleEditorCursorPageChange : function(pageNumber){ + const handleEditorCursorPageChange (pageNumber)=>{ this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + }; - handleBrewRendererPageChange : function(pageNumber){ + const handleBrewRendererPageChange (pageNumber)=>{ this.setState({ currentBrewRendererPageNum: pageNumber }); - }, + }; - handleTextChange : function(text){ + const handleTextChange (text)=>{ this.setState((prevState)=>({ brew : { ...prevState.brew, text: text }, })); - }, - renderNavbar : function(){ + }; + + const renderNavbar = ()=>{ return {this.state.error ? @@ -88,12 +90,12 @@ const HomePage =(props)=>{ ; - }, + }; - render : function(){ - return

+ return ( +
- {this.renderNavbar()} + {renderNavbar()} ; - } +
+ ) }; module.exports = HomePage; From 8cf55932a9392799e5127e33e253d94ea6826c50 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 17:10:20 -0400 Subject: [PATCH 20/48] Fix useEffect and Refs; Update fetchThemeBundle to work with functional --- client/homebrew/pages/homePage/homePage.jsx | 75 +++++++++++---------- shared/helpers.js | 16 ++--- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 1b3ad0608..e09296388 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -22,26 +22,27 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); const HomePage =(props)=>{ - const { - brew = DEFAULT_BREW, - ver = '0.0.0' - } = props; + props = { + brew : DEFAULT_BREW, + ver : '0.0.0', + ...props + }; - const [brew , setBrew] = useState(brew); - const [welcomeText , setWelcomeText] = useState(brew.text); + const [brew , setBrew] = useState(props.brew); + const [welcomeText , setWelcomeText] = useState(props.brew.text); const [error , setError] = useState(undefined); const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle , setThemeBundle] = useState({}); - editor : React.createRef(null), + const editorRef = useRef(null); - componentDidMount : function() { - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); - }, + useEffect(()=>{ + fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme); + }, []); - const handleSave ()=>{ + const handleSave = ()=>{ request.post('/api') .send(this.state.brew) .end((err, res)=>{ @@ -53,24 +54,24 @@ const HomePage =(props)=>{ window.location = `/edit/${brew.editId}`; }); }; - - const handleSplitMove ()=>{ + + const handleSplitMove = ()=>{ this.editor.current.update(); }; - const handleEditorViewPageChange (pageNumber)=>{ + const handleEditorViewPageChange = (pageNumber)=>{ this.setState({ currentEditorViewPageNum: pageNumber }); }; - const handleEditorCursorPageChange (pageNumber)=>{ + const handleEditorCursorPageChange = (pageNumber)=>{ this.setState({ currentEditorCursorPageNum: pageNumber }); }; - const handleBrewRendererPageChange (pageNumber)=>{ + const handleBrewRendererPageChange = (pageNumber)=>{ this.setState({ currentBrewRendererPageNum: pageNumber }); }; - const handleTextChange (text)=>{ + const handleTextChange = (text)=>{ this.setState((prevState)=>({ brew : { ...prevState.brew, text: text }, })); @@ -97,33 +98,33 @@ const HomePage =(props)=>{ {renderNavbar()}
- +
-
+
Save current
diff --git a/shared/helpers.js b/shared/helpers.js index e09b0bdc4..3f91583d6 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -116,27 +116,21 @@ const printCurrentBrew = ()=>{ } }; -const fetchThemeBundle = async (obj, renderer, theme)=>{ +const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ if(!renderer || !theme) return; const res = await request .get(`/api/theme/${renderer}/${theme}`) .catch((err)=>{ - obj.setState({ error: err }); + setError(err) }); if(!res) { - obj.setState((prevState)=>({ - ...prevState, - themeBundle : {} - })); + setThemeBundle({}); return; } const themeBundle = res.body; themeBundle.joinedStyles = themeBundle.styles.map((style)=>``).join('\n\n'); - obj.setState((prevState)=>({ - ...prevState, - themeBundle : themeBundle, - error : null - })); + setThemeBundle(themeBundle); + setError(null); }; const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { From 15c04ef37ed69b19136a19d4049f7710ec7e506d Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 17:14:37 -0400 Subject: [PATCH 21/48] Update homePage.jsx --- client/homebrew/pages/homePage/homePage.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index e09296388..c8a66e732 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -44,7 +44,7 @@ const HomePage =(props)=>{ const handleSave = ()=>{ request.post('/api') - .send(this.state.brew) + .send(brew) .end((err, res)=>{ if(err) { this.setState({ error: err }); @@ -81,7 +81,7 @@ const HomePage =(props)=>{ return {this.state.error ? - : + : null } From 986bfdd00a9843cf17de4ea1a15315a8b008f40d Mon Sep 17 00:00:00 2001 From: David Bolack Date: Sat, 30 Aug 2025 17:37:23 -0500 Subject: [PATCH 22/48] Prevent extra columns --- client/homebrew/brewRenderer/brewRenderer.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 6bcfc87ec..6f2e30c01 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -40,7 +40,7 @@ const BrewPage = (props)=>{ ...props }; const pageRef = useRef(null); - const cleanText = safeHTML(`${props.contents}\n
\n`); + const cleanText = safeHTML(`${props.contents}\n`); useEffect(()=>{ if(!pageRef.current) return; @@ -207,7 +207,8 @@ const BrewRenderer = (props)=>{ } // DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME. - pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) + + pageText += pageText.indexOf(`\n\\column\n`) < 0 ? `\n\n \n\\column\n ` : 0; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) const html = Markdown.render(pageText, index); From da578c53a809c3d9fc9869895883d7f199882753 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 18:50:49 -0400 Subject: [PATCH 23/48] Remove extraneous changes Overcorrecting in the other direction --- client/homebrew/brewRenderer/brewRenderer.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 6f2e30c01..84ce44bec 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -207,8 +207,7 @@ const BrewRenderer = (props)=>{ } // DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME. - - pageText += pageText.indexOf(`\n\\column\n`) < 0 ? `\n\n \n\\column\n ` : 0; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) + pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear) const html = Markdown.render(pageText, index); From 53f6e48f8f73cc9fc872d03bffcf18153cd0e4fe Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 18:51:59 -0400 Subject: [PATCH 24/48] cleanup extra `\n` being added --- client/homebrew/brewRenderer/brewRenderer.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 84ce44bec..407a911c8 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -40,7 +40,7 @@ const BrewPage = (props)=>{ ...props }; const pageRef = useRef(null); - const cleanText = safeHTML(`${props.contents}\n`); + const cleanText = safeHTML(props.contents); useEffect(()=>{ if(!pageRef.current) return; From 6600d9344caa59436cf61664dca4ced2b5d06e53 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 18:53:55 -0400 Subject: [PATCH 25/48] Revert "Add missing punctuation and sentence structure characters to mustache style assignment regex" --- client/homebrew/editor/editor.jsx | 2 +- shared/naturalcrit/markdown.js | 23 ++++++----------------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index fc20f2be4..8d331e46e 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -12,7 +12,7 @@ const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME'; -const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n]*})?$)/m; +const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/; const DEFAULT_STYLE_TEXT = dedent` /*=======--- Example CSS styling ---=======*/ diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index c07d879ec..78107dcf4 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -185,7 +185,7 @@ const mustacheSpans = { start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token - const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-()#%=?. \&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\=]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; + const inlineRegex = /{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *|}}/g; const match = completeSpan.exec(src); if(match) { //Find closing delimiter @@ -241,8 +241,8 @@ const mustacheDivs = { level : 'block', start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const completeBlock = /^ *{{[^\n]* *\n.*\n *}}/s; // Regex for the complete token - const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%. ]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; + const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token + const blockRegex = /^ *{{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1 *$|^ *}}$/gm; const match = completeBlock.exec(src); if(match) { //Find closing delimiter @@ -297,7 +297,7 @@ const mustacheInjectInline = { level : 'inline', start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?.\&\:\!\@\$\^\*\<\>\;\:\[\]\{\}\-\_\+\= ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/g; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/g; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -343,7 +343,7 @@ const mustacheInjectBlock = { level : 'block', start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-()#%=?.& ]*"|[\w\-()#%.]*)|[^"=':{}\s]*)*))\1}/ym; + const inlineRegex = /^ *{(?=((?:[:=](?:"['\w,\-+*/()#%=?. ]*"|[\w\-+*/()#%.]*)|[^"=':{}\s]*)*))\1}/ym; const match = inlineRegex.exec(src); if(match) { const lastToken = tokens[tokens.length - 1]; @@ -735,17 +735,6 @@ const voidTags = new Set([ 'input', 'keygen', 'link', 'meta', 'param', 'source' ]); -const notInside = (string, stringMatch1, stringMatch2)=> { - const pos1 = string.indexOf(stringMatch1); - const pos2 = string.indexOf(stringMatch2); - - if(((pos1 > 0) && (pos2 == -1)) || - ((pos1 > 0) && (pos2 > 0) && (pos2 > pos1))) { - return true; - } - return false; -}; - const processStyleTags = (string)=>{ //split tags up. quotes can only occur right after : or =. //TODO: can we simplify to just split on commas? @@ -753,7 +742,7 @@ const processStyleTags = (string)=>{ const id = _.remove(tags, (tag)=>tag.startsWith('#')).map((tag)=>tag.slice(1))[0] || null; const classes = _.remove(tags, (tag)=>(!tag.includes(':')) && (!tag.includes('='))).join(' ') || null; - const attributes = _.remove(tags, (tag)=>(notInside(tag, '=', ':'))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')) + const attributes = _.remove(tags, (tag)=>(tag.includes('='))).map((tag)=>tag.replace(/="?([^"]*)"?/g, '="$1"')) ?.filter((attr)=>!attr.startsWith('class="') && !attr.startsWith('style="') && !attr.startsWith('id="')) .reduce((obj, attr)=>{ const index = attr.indexOf('='); From 518a3434bec39668998c01166aba2618d64b1033 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 19:02:39 -0400 Subject: [PATCH 26/48] Changes fetchThemeBundle helper to not need "this" parameter Looks a bit ugly but this is temporary toward converting edit/home/new into functional components --- client/homebrew/brewRenderer/brewRenderer.jsx | 4 ++-- client/homebrew/pages/editPage/editPage.jsx | 4 ++-- client/homebrew/pages/homePage/homePage.jsx | 2 +- client/homebrew/pages/newPage/newPage.jsx | 4 ++-- client/homebrew/pages/sharePage/sharePage.jsx | 20 ++++++------------- shared/helpers.js | 16 +++++---------- 6 files changed, 18 insertions(+), 32 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 6bcfc87ec..7a101e9f9 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -39,8 +39,8 @@ const BrewPage = (props)=>{ index : 0, ...props }; - const pageRef = useRef(null); - const cleanText = safeHTML(`${props.contents}\n
\n`); + const pageRef = useRef(null); + const cleanText = safeHTML(props.contents); useEffect(()=>{ if(!pageRef.current) return; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d1f0ed21c..ad99948a9 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -97,7 +97,7 @@ const EditPage = createClass({ htmlErrors : Markdown.validate(prevState.brew.text) })); - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); + fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); document.addEventListener('keydown', this.handleControlKeys); }, @@ -173,7 +173,7 @@ const EditPage = createClass({ handleMetaChange : function(metadata, field=undefined){ if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle(this, metadata.renderer, metadata.theme); + fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme); this.setState((prevState)=>({ brew : { diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index eac0216fd..3b8fdbf01 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -44,7 +44,7 @@ const HomePage = createClass({ editor : React.createRef(null), componentDidMount : function() { - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); + fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); }, handleSave : function(){ diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index ab7c22541..135794a81 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -80,7 +80,7 @@ const NewPage = createClass({ saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle) }); - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); + fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); localStorage.setItem(BREWKEY, brew.text); if(brew.style) @@ -154,7 +154,7 @@ const NewPage = createClass({ handleMetaChange : function(metadata, field=undefined){ if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle(this, metadata.renderer, metadata.theme); + fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme); this.setState((prevState)=>({ brew : { ...prevState.brew, ...metadata }, diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index e9c5540a2..50104a665 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -17,15 +17,11 @@ const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpe const SharePage = (props)=>{ const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props; - const [state, setState] = useState({ - themeBundle : {}, - currentBrewRendererPageNum : 1, - }); + const [themeBundle, setThemeBundle] = useState({}); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const handleBrewRendererPageChange = useCallback((pageNumber)=>{ - setState((prevState)=>({ - currentBrewRendererPageNum : pageNumber, - ...prevState })); + setCurrentBrewRendererPageNum(pageNumber); }, []); const handleControlKeys = (e)=>{ @@ -40,11 +36,7 @@ const SharePage = (props)=>{ useEffect(()=>{ document.addEventListener('keydown', handleControlKeys); - fetchThemeBundle( - { setState }, - brew.renderer, - brew.theme - ); + fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme); return ()=>{ document.removeEventListener('keydown', handleControlKeys); @@ -114,9 +106,9 @@ const SharePage = (props)=>{ lang={brew.lang} renderer={brew.renderer} theme={brew.theme} - themeBundle={state.themeBundle} + themeBundle={themeBundle} onPageChange={handleBrewRendererPageChange} - currentBrewRendererPageNum={state.currentBrewRendererPageNum} + currentBrewRendererPageNum={currentBrewRendererPageNum} allowPrint={true} />
diff --git a/shared/helpers.js b/shared/helpers.js index e09b0bdc4..3f91583d6 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -116,27 +116,21 @@ const printCurrentBrew = ()=>{ } }; -const fetchThemeBundle = async (obj, renderer, theme)=>{ +const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ if(!renderer || !theme) return; const res = await request .get(`/api/theme/${renderer}/${theme}`) .catch((err)=>{ - obj.setState({ error: err }); + setError(err) }); if(!res) { - obj.setState((prevState)=>({ - ...prevState, - themeBundle : {} - })); + setThemeBundle({}); return; } const themeBundle = res.body; themeBundle.joinedStyles = themeBundle.styles.map((style)=>``).join('\n\n'); - obj.setState((prevState)=>({ - ...prevState, - themeBundle : themeBundle, - error : null - })); + setThemeBundle(themeBundle); + setError(null); }; const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { From 8671404bdc405b7efc2cf017b35d421326adc407 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 19:35:22 -0400 Subject: [PATCH 27/48] Refactor ErrorNavItem to not need "this" parameter Toward making edit/new/home pages functional, which do not have "this" --- client/homebrew/navbar/error-navitem.jsx | 269 +++++++++----------- client/homebrew/pages/editPage/editPage.jsx | 9 +- client/homebrew/pages/homePage/homePage.jsx | 10 +- client/homebrew/pages/newPage/newPage.jsx | 9 +- client/homebrew/pages/userPage/userPage.jsx | 6 +- 5 files changed, 155 insertions(+), 148 deletions(-) diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx index ec72ace7d..6d9bec444 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -1,157 +1,138 @@ require('./error-navitem.less'); const React = require('react'); const Nav = require('naturalcrit/nav/nav.jsx'); -const createClass = require('create-react-class'); -const ErrorNavItem = createClass({ - getDefaultProps : function() { - return { - error : '', - parent : null - }; - }, - render : function() { - const clearError = ()=>{ - const state = { - error : null - }; - if(this.props.parent.state.isSaving) { - state.isSaving = false; - } - this.props.parent.setState(state); - }; +const ErrorNavItem = ({error = '', clearError})=>{ + const response = error.response; + const errorCode = error.code + const status = response?.status; + const HBErrorCode = response?.body?.HBErrorCode; + const message = response?.body?.message; - const error = this.props.error; - const response = error.response; - const status = response?.status; - const errorCode = error.code - const HBErrorCode = response?.body?.HBErrorCode; - const message = response?.body?.message; - let errMsg = ''; - try { - errMsg += `${error.toString()}\n\n`; - errMsg += `\`\`\`\n${error.stack}\n`; - errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; - console.log(errMsg); - } catch (e){} - - if(status === 409) { - return - Oops! -
- {message ?? 'Conflict: please refresh to get latest changes'} -
-
; - } - - if(status === 412) { - return - Oops! -
- {message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'} -
-
; - } - - if(HBErrorCode === '04') { - return - Oops! -
- You are no longer signed in as an author of - this brew! Were you signed out from a different - window? Visit our log in page, then try again! -

- -
- Sign In -
-
-
- Not Now -
-
-
; - } - - if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') { - return - Oops! -
- Can't save because your Google Drive seems to be full! -
-
; - } - - if(response?.req.url.match(/^\/api.*Google.*$/m)){ - return - Oops! -
- Looks like your Google credentials have - expired! Visit our log in page to sign out - and sign back in with Google, - then try saving again! -

- -
- Sign In -
-
-
- Not Now -
-
-
; - } - - if(HBErrorCode === '09') { - return - Oops! -
- Looks like there was a problem retreiving - the theme, or a theme that it inherits, - for this brew. Verify that brew - {response.body.brewId} still exists! -
-
; - } - - if(HBErrorCode === '10') { - return - Oops! -
- Looks like the brew you have selected - as a theme is not tagged for use as a - theme. Verify that - brew - {response.body.brewId} has the meta:theme tag! -
-
; - } - - if(errorCode === 'ECONNABORTED') { - return - Oops! -
- The request to the server was interrupted or timed out. - This can happen due to a network issue, or if - trying to save a particularly large brew. - Please check your internet connection and try again. -
-
; - } + let errMsg = ''; + try { + errMsg += `${error.toString()}\n\n`; + errMsg += `\`\`\`\n${error.stack}\n`; + errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; + console.log(errMsg); + } catch (e){} + if(status === 409) { return Oops! -
- Looks like there was a problem saving.
- Report the issue - here - . +
+ {message ?? 'Conflict: please refresh to get latest changes'}
; } -}); + + if(status === 412) { + return + Oops! +
+ {message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'} +
+
; + } + + if(HBErrorCode === '04') { + return + Oops! +
+ You are no longer signed in as an author of + this brew! Were you signed out from a different + window? Visit our log in page, then try again! +

+ +
+ Sign In +
+
+
+ Not Now +
+
+
; + } + + if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') { + return + Oops! +
+ Can't save because your Google Drive seems to be full! +
+
; + } + + if(response?.req.url.match(/^\/api.*Google.*$/m)){ + return + Oops! +
+ Looks like your Google credentials have + expired! Visit our log in page to sign out + and sign back in with Google, + then try saving again! +

+ +
+ Sign In +
+
+
+ Not Now +
+
+
; + } + + if(HBErrorCode === '09') { + return + Oops! +
+ Looks like there was a problem retreiving + the theme, or a theme that it inherits, + for this brew. Verify that brew + {response.body.brewId} still exists! +
+
; + } + + if(HBErrorCode === '10') { + return + Oops! +
+ Looks like the brew you have selected + as a theme is not tagged for use as a + theme. Verify that + brew + {response.body.brewId} has the meta:theme tag! +
+
; + } + + if(errorCode === 'ECONNABORTED') { + return + Oops! +
+ The request to the server was interrupted or timed out. + This can happen due to a network issue, or if + trying to save a particularly large brew. + Please check your internet connection and try again. +
+
; + } + + return + Oops! +
+ Looks like there was a problem saving.
+ Report the issue + here + . +
+
; +}; module.exports = ErrorNavItem; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d1f0ed21c..68b1f0777 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -438,6 +438,13 @@ const EditPage = createClass({ return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; }, + clearError : function(){ + setState({ + error : null, + isSaving : false + }) + }, + renderNavbar : function(){ const shareLink = this.processShareId(); @@ -449,7 +456,7 @@ const EditPage = createClass({ {this.renderGoogleDriveIcon()} {this.state.error ? - : + : {this.renderSaveButton()} {this.renderAutoSaveButton()} diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index eac0216fd..15150a5bd 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -80,11 +80,19 @@ const HomePage = createClass({ brew : { ...prevState.brew, text: text }, })); }, + + clearError : function(){ + setState({ + error : null, + isSaving : false + }) + }, + renderNavbar : function(){ return {this.state.error ? - : + : null } diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index ab7c22541..46d7c2612 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -211,6 +211,13 @@ const NewPage = createClass({ } }, + clearError : function(){ + setState({ + error : null, + isSaving : false + }) + }, + renderNavbar : function(){ return @@ -220,7 +227,7 @@ const NewPage = createClass({ {this.state.error ? - : + : this.renderSaveButton() } diff --git a/client/homebrew/pages/userPage/userPage.jsx b/client/homebrew/pages/userPage/userPage.jsx index f6fae639d..e4a8b0b4e 100644 --- a/client/homebrew/pages/userPage/userPage.jsx +++ b/client/homebrew/pages/userPage/userPage.jsx @@ -39,10 +39,14 @@ const UserPage = (props)=>{ }] : []) ]; + const clearError = ()=>{ + setError(null); + }; + const navItems = ( - {error && ()} + {error && ()} From 9c336062c61237b22b33fc183cab93e4804ec034 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 19:39:15 -0400 Subject: [PATCH 28/48] Fix typo --- client/homebrew/pages/editPage/editPage.jsx | 2 +- client/homebrew/pages/homePage/homePage.jsx | 2 +- client/homebrew/pages/newPage/newPage.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 9a85c5975..51196a444 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -456,7 +456,7 @@ const EditPage = createClass({ {this.renderGoogleDriveIcon()} {this.state.error ? - : + : {this.renderSaveButton()} {this.renderAutoSaveButton()} diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 9b7bad150..97c893cf5 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -92,7 +92,7 @@ const HomePage = createClass({ return {this.state.error ? - : + : null } diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index aab77c25c..c24128a93 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -227,7 +227,7 @@ const NewPage = createClass({ {this.state.error ? - : + : this.renderSaveButton() } From 93b86632fce8ecfc870a4a48c2b0633f2f6c6768 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 30 Aug 2025 20:14:29 -0400 Subject: [PATCH 29/48] Change from require to import --- client/homebrew/pages/homePage/homePage.jsx | 79 ++++++++++----------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index ab1eee122..84967b1ff 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -1,25 +1,25 @@ -require('./homePage.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const cx = require('classnames'); -import request from '../../utils/request-middleware.js'; -const { Meta } = require('vitreum/headtags'); +import './homePage.less'; -const Nav = require('naturalcrit/nav/nav.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); -const NewBrewItem = require('../../navbar/newbrew.navitem.jsx'); -const HelpNavItem = require('../../navbar/help.navitem.jsx'); -const VaultNavItem = require('../../navbar/vault.navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const AccountNavItem = require('../../navbar/account.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const { fetchThemeBundle } = require('../../../../shared/helpers.js'); +import React from 'react'; +import { useEffect, useState, useRef } from 'react'; +import request from '../../utils/request-middleware.js'; +import { Meta } from 'vitreum/headtags'; -const SplitPane = require('client/components/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import Nav from 'naturalcrit/nav/nav.jsx'; +import Navbar from '../../navbar/navbar.jsx'; +import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; +import HelpNavItem from '../../navbar/help.navitem.jsx'; +import VaultNavItem from '../../navbar/vault.navitem.jsx'; +import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; +import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ErrorNavItem from '../../navbar/error-navitem.jsx'; +import { fetchThemeBundle } from '../../../../shared/helpers.js'; -const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; + +import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; const HomePage =(props)=>{ props = { @@ -35,6 +35,7 @@ const HomePage =(props)=>{ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle , setThemeBundle] = useState({}); + const [isSaving , setIsSaving] = useState(false); const editorRef = useRef(null); @@ -42,53 +43,49 @@ const HomePage =(props)=>{ fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme); }, []); - const handleSave = ()=>{ + const save = ()=>{ request.post('/api') .send(brew) .end((err, res)=>{ if(err) { - this.setState({ error: err }); + setError(err); return; } - const brew = res.body; - window.location = `/edit/${brew.editId}`; + const saved = res.body; + window.location = `/edit/${saved.editId}`; }); }; const handleSplitMove = ()=>{ - this.editor.current.update(); + editorRef.current.update(); }; const handleEditorViewPageChange = (pageNumber)=>{ - this.setState({ currentEditorViewPageNum: pageNumber }); + setCurrentEditorViewPageNum(pageNumber); }; - + const handleEditorCursorPageChange = (pageNumber)=>{ - this.setState({ currentEditorCursorPageNum: pageNumber }); + setCurrentEditorCursorPageNum(pageNumber); }; - + const handleBrewRendererPageChange = (pageNumber)=>{ - this.setState({ currentBrewRendererPageNum: pageNumber }); + setCurrentBrewRendererPageNum(pageNumber); }; const handleTextChange = (text)=>{ - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - })); + setBrew((prevBrew) => ({ ...prevBrew, text })); }; const clearError = ()=>{ - setState({ - error : null, - isSaving : false - }) + setError(null); + setIsSaving(false); }; - renderNavbar : function(){ - return + const renderNavbar = ()=>{ + return - {this.state.error ? - : + {error ? + : null } @@ -131,7 +128,7 @@ const HomePage =(props)=>{ />
-
+
Save current
From 1aeded648e200039332744fa86d21c92682f9bef Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 2 Sep 2025 22:21:49 -0400 Subject: [PATCH 30/48] make newPage functional --- client/homebrew/pages/newPage/newPage.jsx | 367 ++++++++++------------ 1 file changed, 168 insertions(+), 199 deletions(-) diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index c24128a93..bb21441cf 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -1,282 +1,251 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ -require('./newPage.less'); -const React = require('react'); -const createClass = require('create-react-class'); -import request from '../../utils/request-middleware.js'; +import './newPage.less'; -import Markdown from 'naturalcrit/markdown.js'; +import React, { useState, useEffect, useRef } from 'react'; +import request from '../../utils/request-middleware.js'; +import Markdown from 'naturalcrit/markdown.js'; -const Nav = require('naturalcrit/nav/nav.jsx'); -const PrintNavItem = require('../../navbar/print.navitem.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); -const AccountNavItem = require('../../navbar/account.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const HelpNavItem = require('../../navbar/help.navitem.jsx'); +import Nav from 'naturalcrit/nav/nav.jsx'; +import Navbar from '../../navbar/navbar.jsx'; +import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ErrorNavItem from '../../navbar/error-navitem.jsx'; +import HelpNavItem from '../../navbar/help.navitem.jsx'; +import PrintNavItem from '../../navbar/print.navitem.jsx'; +import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; -const SplitPane = require('client/components/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); -const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); +import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; const BREWKEY = 'homebrewery-new'; const STYLEKEY = 'homebrewery-new-style'; const METAKEY = 'homebrewery-new-meta'; -let SAVEKEY; +const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; +const NewPage = (props) => { + props = { + brew: DEFAULT_BREW, + ...props + }; -const NewPage = createClass({ - displayName : 'NewPage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW + const [currentBrew , setCurrentBrew ] = useState(props.brew); + const [isSaving , setIsSaving ] = useState(false); + const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false); + const [error , setError ] = useState(null); + const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); + const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); + const [themeBundle , setThemeBundle ] = useState({}); + + const editorRef = useRef(null); + + useEffect(() => { + document.addEventListener('keydown', handleControlKeys); + loadBrew(); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); + + return () => { + document.removeEventListener('keydown', handleControlKeys); }; - }, + }, []); - getInitialState : function() { - const brew = this.props.brew; - - return { - brew : brew, - isSaving : false, - saveGoogle : (global.account && global.account.googleId ? true : false), - error : null, - htmlErrors : Markdown.validate(brew.text), - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - themeBundle : {} - }; - }, - - editor : React.createRef(null), - - componentDidMount : function() { - document.addEventListener('keydown', this.handleControlKeys); - - const brew = this.state.brew; - - if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser + const loadBrew = ()=>{ + const brew = { ...currentBrew }; + if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser const brewStorage = localStorage.getItem(BREWKEY); const styleStorage = localStorage.getItem(STYLEKEY); - const metaStorage = JSON.parse(localStorage.getItem(METAKEY)); + const metaStorage = JSON.parse(localStorage.getItem(METAKEY)); - brew.text = brewStorage ?? brew.text; - brew.style = styleStorage ?? brew.style; - // brew.title = metaStorage?.title || this.state.brew.title; - // brew.description = metaStorage?.description || this.state.brew.description; + brew.text = brewStorage ?? brew.text; + brew.style = styleStorage ?? brew.style; brew.renderer = metaStorage?.renderer ?? brew.renderer; brew.theme = metaStorage?.theme ?? brew.theme; brew.lang = metaStorage?.lang ?? brew.lang; } - SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; - this.setState({ - brew : brew, - saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle) - }); - - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); + setCurrentBrew(brew); + setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle); localStorage.setItem(BREWKEY, brew.text); if(brew.style) localStorage.setItem(STYLEKEY, brew.style); - localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang })); - if(window.location.pathname != '/new') { + localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang })); + if(window.location.pathname !== '/new') window.history.replaceState({}, window.location.title, '/new/'); - } - }, - componentWillUnmount : function() { - document.removeEventListener('keydown', this.handleControlKeys); - }, + }; - handleControlKeys : function(e){ - if(!(e.ctrlKey || e.metaKey)) return; + const handleControlKeys = (e) => { + if (!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; - if(e.keyCode == S_KEY) this.save(); - if(e.keyCode == P_KEY) printCurrentBrew(); - if(e.keyCode == P_KEY || e.keyCode == S_KEY){ - e.stopPropagation(); + if (e.keyCode === S_KEY) save(); + if (e.keyCode === P_KEY) printCurrentBrew(); + if (e.keyCode === S_KEY || e.keyCode === P_KEY) { e.preventDefault(); + e.stopPropagation(); } - }, + }; - handleSplitMove : function(){ - this.editor.current.update(); - }, + const handleSplitMove = ()=>{ + editorRef.current.update(); + }; - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, + const handleEditorViewPageChange = (pageNumber)=>{ + setCurrentEditorViewPageNum(pageNumber); + }; + + const handleEditorCursorPageChange = (pageNumber)=>{ + setCurrentEditorCursorPageNum(pageNumber); + }; + + const handleBrewRendererPageChange = (pageNumber)=>{ + setCurrentBrewRendererPageNum(pageNumber); + }; - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + const handleTextChange = (text)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(text); - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: pageNumber }); - }, - - handleTextChange : function(text){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(text); - - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - })); + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); localStorage.setItem(BREWKEY, text); - }, + }; - handleStyleChange : function(style){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style }, - })); + const handleStyleChange = (style) => { + setCurrentBrew(prevBrew => ({ ...prevBrew, style })); localStorage.setItem(STYLEKEY, style); - }, + }; - handleSnipChange : function(snippet){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + const handleSnipChange = (snippet)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(snippet); - this.setState((prevState)=>({ - brew : { ...prevState.brew, snippets: snippet }, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); + }; - handleMetaChange : function(metadata, field=undefined){ - if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme); + const handleMetaChange = (metadata, field = undefined) => { + if (field === 'theme' || field === 'renderer') + fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - this.setState((prevState)=>({ - brew : { ...prevState.brew, ...metadata }, - }), ()=>{ - localStorage.setItem(METAKEY, JSON.stringify({ - // 'title' : this.state.brew.title, - // 'description' : this.state.brew.description, - 'renderer' : this.state.brew.renderer, - 'theme' : this.state.brew.theme, - 'lang' : this.state.brew.lang - })); - }); - ; - }, + setCurrentBrew(prev => ({ ...prev, ...metadata })); + localStorage.setItem(METAKEY, JSON.stringify({ + renderer : metadata.renderer, + theme : metadata.theme, + lang : metadata.lang + })); + }; - save : async function(){ - this.setState({ - isSaving : true - }); + const save = async () => { + setIsSaving(true); - let brew = this.state.brew; - // Split out CSS to Style if CSS codefence exists - if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) { - const index = brew.text.indexOf('```\n\n'); - brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`; - brew.text = brew.text.slice(index + 5); - } + let updatedBrew = { ...currentBrew }; + splitTextStyleAndMetadata(updatedBrew); + + const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm; + updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1; - brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; const res = await request - .post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`) - .send(brew) - .catch((err)=>{ - this.setState({ isSaving: false, error: err }); + .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) + .send(updatedBrew) + .catch((err) => { + setIsSaving(false); + setError(err); }); - if(!res) return; - brew = res.body; + setIsSaving(false) + if (!res) return; + + const savedBrew = res.body; + localStorage.removeItem(BREWKEY); localStorage.removeItem(STYLEKEY); localStorage.removeItem(METAKEY); - window.location = `/edit/${brew.editId}`; - }, + window.location = `/edit/${savedBrew.editId}`; + }; - renderSaveButton : function(){ - if(this.state.isSaving){ + const renderSaveButton = ()=>{ + if(isSaving){ return save... ; } else { - return + return save ; } - }, + }; - clearError : function(){ - setState({ - error : null, - isSaving : false - }) - }, - - renderNavbar : function(){ - return + const clearError = ()=>{ + setError(null); + setIsSaving(false); + }; + const renderNavbar = () => ( + - {this.state.brew.title} + {currentBrew.title} - {this.state.error ? - : - this.renderSaveButton() - } + {error + ? + : renderSaveButton()} - ; - }, + + ); - render : function(){ - return
- {this.renderNavbar()} + return ( +
+ {renderNavbar()}
- +
-
; - } -}); +
+ ); +}; module.exports = NewPage; From 172c11646a88d12c9755be4ca01b8380602a3c9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:29:06 +0000 Subject: [PATCH 31/48] Bump jest from 30.0.5 to 30.1.1 in the dev-dependencies group Bumps the dev-dependencies group with 1 update: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest). Updates `jest` from 30.0.5 to 30.1.1 - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.1.1/packages/jest) --- updated-dependencies: - dependency-name: jest dependency-version: 30.1.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 489 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 246 insertions(+), 245 deletions(-) diff --git a/package-lock.json b/package-lock.json index 430fb6778..0ef60655d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,7 @@ "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", - "jest": "^30.0.5", + "jest": "^30.1.3", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", @@ -1824,9 +1824,10 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -1925,14 +1926,14 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.4", + "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, @@ -1945,9 +1946,9 @@ "optional": true }, "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "dev": true, "license": "MIT", "optional": true, @@ -1964,9 +1965,9 @@ "optional": true }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "optional": true, @@ -2210,9 +2211,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { @@ -2412,16 +2413,16 @@ } }, "node_modules/@jest/console": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", - "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.2.tgz", + "integrity": "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.0.5", + "jest-message-util": "30.1.0", "jest-util": "30.0.5", "slash": "^3.0.0" }, @@ -2430,17 +2431,17 @@ } }, "node_modules/@jest/core": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", - "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.3.tgz", + "integrity": "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.0.5", + "@jest/console": "30.1.2", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/reporters": "30.1.3", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", @@ -2449,18 +2450,18 @@ "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-changed-files": "30.0.5", - "jest-config": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", + "jest-config": "30.1.3", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-resolve-dependencies": "30.0.5", - "jest-runner": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", + "jest-resolve": "30.1.3", + "jest-resolve-dependencies": "30.1.3", + "jest-runner": "30.1.3", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "jest-watcher": "30.0.5", + "jest-validate": "30.1.0", + "jest-watcher": "30.1.3", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0" @@ -2488,13 +2489,13 @@ } }, "node_modules/@jest/environment": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", - "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.2.tgz", + "integrity": "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.0.5", + "@jest/fake-timers": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "jest-mock": "30.0.5" @@ -2504,43 +2505,43 @@ } }, "node_modules/@jest/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.0.5", - "jest-snapshot": "30.0.5" + "expect": "30.1.2", + "jest-snapshot": "30.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", - "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1" + "@jest/get-type": "30.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", - "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.2.tgz", + "integrity": "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.0.5", + "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-util": "30.0.5" }, @@ -2549,9 +2550,9 @@ } }, "node_modules/@jest/get-type": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", "engines": { @@ -2559,14 +2560,14 @@ } }, "node_modules/@jest/globals": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", - "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.2.tgz", + "integrity": "sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", "@jest/types": "30.0.5", "jest-mock": "30.0.5" }, @@ -2589,16 +2590,16 @@ } }, "node_modules/@jest/reporters": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", - "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.3.tgz", + "integrity": "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/console": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", @@ -2612,9 +2613,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.5", + "jest-message-util": "30.1.0", "jest-util": "30.0.5", - "jest-worker": "30.0.5", + "jest-worker": "30.1.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -2692,9 +2693,9 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", - "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.2.tgz", + "integrity": "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==", "dev": true, "license": "MIT", "dependencies": { @@ -2723,13 +2724,13 @@ } }, "node_modules/@jest/test-result": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", - "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.3.tgz", + "integrity": "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.0.5", + "@jest/console": "30.1.2", "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" @@ -2739,15 +2740,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", - "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.3.tgz", + "integrity": "sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.0.5", + "@jest/test-result": "30.1.3", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", + "jest-haste-map": "30.1.0", "slash": "^3.0.0" }, "engines": { @@ -2755,9 +2756,9 @@ } }, "node_modules/@jest/transform": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", - "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", + "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2769,7 +2770,7 @@ "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", + "jest-haste-map": "30.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", @@ -2954,9 +2955,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -3057,13 +3058,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/estree": { @@ -3107,13 +3108,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/stack-utils": { @@ -4041,13 +4042,13 @@ } }, "node_modules/babel-jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", - "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", + "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.0.5", + "@jest/transform": "30.1.2", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.0", "babel-preset-jest": "30.0.1", @@ -4151,9 +4152,9 @@ "license": "0BSD" }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -4174,7 +4175,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -6447,16 +6448,16 @@ "license": "MIT" }, "node_modules/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-util": "30.0.5" }, @@ -8559,9 +8560,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8607,16 +8608,16 @@ } }, "node_modules/jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", - "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", + "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.0.5", + "@jest/core": "30.1.3", "@jest/types": "30.0.5", "import-local": "^3.2.0", - "jest-cli": "30.0.5" + "jest-cli": "30.1.3" }, "bin": { "jest": "bin/jest.js" @@ -8649,26 +8650,26 @@ } }, "node_modules/jest-circus": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", - "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", + "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/test-result": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", + "jest-each": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", "jest-util": "30.0.5", "p-limit": "^3.1.0", "pretty-format": "30.0.5", @@ -8681,21 +8682,21 @@ } }, "node_modules/jest-cli": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", - "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.3.tgz", + "integrity": "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.0.5", - "@jest/test-result": "30.0.5", + "@jest/core": "30.1.3", + "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.0.5", + "jest-config": "30.1.3", "jest-util": "30.0.5", - "jest-validate": "30.0.5", + "jest-validate": "30.1.0", "yargs": "^17.7.2" }, "bin": { @@ -8714,31 +8715,31 @@ } }, "node_modules/jest-config": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", - "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", + "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.5", + "@jest/test-sequencer": "30.1.3", "@jest/types": "30.0.5", - "babel-jest": "30.0.5", + "babel-jest": "30.1.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.0.5", + "jest-circus": "30.1.3", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", + "jest-environment-node": "30.1.2", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-runner": "30.0.5", + "jest-resolve": "30.1.3", + "jest-runner": "30.1.3", "jest-util": "30.0.5", - "jest-validate": "30.0.5", + "jest-validate": "30.1.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", "pretty-format": "30.0.5", @@ -8813,14 +8814,14 @@ } }, "node_modules/jest-diff": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", - "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "pretty-format": "30.0.5" }, @@ -8842,13 +8843,13 @@ } }, "node_modules/jest-each": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", - "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", + "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "@jest/types": "30.0.5", "chalk": "^4.1.2", "jest-util": "30.0.5", @@ -8859,19 +8860,19 @@ } }, "node_modules/jest-environment-node": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", - "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", + "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "jest-mock": "30.0.5", "jest-util": "30.0.5", - "jest-validate": "30.0.5" + "jest-validate": "30.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -8885,9 +8886,9 @@ "license": "MIT" }, "node_modules/jest-haste-map": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", - "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", "dev": true, "license": "MIT", "dependencies": { @@ -8898,7 +8899,7 @@ "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", - "jest-worker": "30.0.5", + "jest-worker": "30.1.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -8910,13 +8911,13 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", - "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", + "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "pretty-format": "30.0.5" }, "engines": { @@ -8924,15 +8925,15 @@ } }, "node_modules/jest-matcher-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", - "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.0.5", + "jest-diff": "30.1.2", "pretty-format": "30.0.5" }, "engines": { @@ -8940,9 +8941,9 @@ } }, "node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", "dev": true, "license": "MIT", "dependencies": { @@ -9004,18 +9005,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", - "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", + "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", + "jest-haste-map": "30.1.0", "jest-pnp-resolver": "^1.2.3", "jest-util": "30.0.5", - "jest-validate": "30.0.5", + "jest-validate": "30.1.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -9024,30 +9025,30 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", - "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.3.tgz", + "integrity": "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.5" + "jest-snapshot": "30.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", - "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", + "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.0.5", - "@jest/environment": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/console": "30.1.2", + "@jest/environment": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", @@ -9055,15 +9056,15 @@ "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-leak-detector": "30.0.5", - "jest-message-util": "30.0.5", - "jest-resolve": "30.0.5", - "jest-runtime": "30.0.5", + "jest-environment-node": "30.1.2", + "jest-haste-map": "30.1.0", + "jest-leak-detector": "30.1.0", + "jest-message-util": "30.1.0", + "jest-resolve": "30.1.3", + "jest-runtime": "30.1.3", "jest-util": "30.0.5", - "jest-watcher": "30.0.5", - "jest-worker": "30.0.5", + "jest-watcher": "30.1.3", + "jest-worker": "30.1.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -9072,18 +9073,18 @@ } }, "node_modules/jest-runtime": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", - "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", + "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/globals": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/globals": "30.1.2", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", @@ -9091,12 +9092,12 @@ "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-snapshot": "30.0.5", + "jest-resolve": "30.1.3", + "jest-snapshot": "30.1.2", "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" @@ -9153,9 +9154,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", - "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", + "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -9164,18 +9165,18 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.1.2", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "babel-preset-current-node-syntax": "^1.1.0", "chalk": "^4.1.2", - "expect": "30.0.5", + "expect": "30.1.2", "graceful-fs": "^4.2.11", - "jest-diff": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", + "jest-diff": "30.1.2", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", "jest-util": "30.0.5", "pretty-format": "30.0.5", "semver": "^7.7.2", @@ -9230,13 +9231,13 @@ } }, "node_modules/jest-validate": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", - "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", + "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "@jest/types": "30.0.5", "camelcase": "^6.3.0", "chalk": "^4.1.2", @@ -9261,13 +9262,13 @@ } }, "node_modules/jest-watcher": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", - "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", + "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.0.5", + "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", @@ -9281,9 +9282,9 @@ } }, "node_modules/jest-worker": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", - "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", "dev": true, "license": "MIT", "dependencies": { @@ -10604,9 +10605,9 @@ } }, "node_modules/napi-postinstall": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", - "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", "dev": true, "license": "MIT", "bin": { @@ -14158,9 +14159,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 5c6fb4efc..b55b96b31 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", - "jest": "^30.0.5", + "jest": "^30.1.3", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", From 32f9a44acf176c9c20a5de1dabb5176c05c73ecb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 03:01:23 +0000 Subject: [PATCH 32/48] Bump the dev-dependencies group with 3 updates Bumps the dev-dependencies group with 3 updates: [eslint](https://github.com/eslint/eslint), [stylelint](https://github.com/stylelint/stylelint) and [stylelint-config-recess-order](https://github.com/stormwarning/stylelint-config-recess-order). Updates `eslint` from 9.34.0 to 9.35.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.34.0...v9.35.0) Updates `stylelint` from 16.23.1 to 16.24.0 - [Release notes](https://github.com/stylelint/stylelint/releases) - [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint/compare/16.23.1...16.24.0) Updates `stylelint-config-recess-order` from 7.2.0 to 7.3.0 - [Release notes](https://github.com/stormwarning/stylelint-config-recess-order/releases) - [Changelog](https://github.com/stormwarning/stylelint-config-recess-order/blob/main/CHANGELOG.md) - [Commits](https://github.com/stormwarning/stylelint-config-recess-order/compare/v7.2.0...v7.3.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.35.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: stylelint dependency-version: 16.24.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: stylelint-config-recess-order dependency-version: 7.3.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] --- package-lock.json | 81 ++++++++++++++++++++++++----------------------- package.json | 6 ++-- 2 files changed, 45 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ef60655d..fb815cdc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.34.0", + "eslint": "^9.35.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", @@ -73,8 +73,8 @@ "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.23.1", - "stylelint-config-recess-order": "^7.2.0", + "stylelint": "^16.24.0", + "stylelint-config-recess-order": "^7.3.0", "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" }, @@ -1984,17 +1984,20 @@ "optional": true }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", + "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -2083,9 +2086,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -4652,14 +4655,14 @@ } }, "node_modules/cacheable": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.3.tgz", - "integrity": "sha512-M6p10iJ/VT0wT7TLIGUnm958oVrU2cUK8pQAVU21Zu7h8rbk/PeRtRWrvHJBql97Bhzk3g1N6+2VKC+Rjxna9Q==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.4.tgz", + "integrity": "sha512-Gd7ccIUkZ9TE2odLQVS+PDjIvQCdJKUlLdJRVvZu0aipj07Qfx+XIej7hhDrKGGoIxV5m5fT/kOJNJPQhQneRg==", "dev": true, "license": "MIT", "dependencies": { - "hookified": "^1.10.0", - "keyv": "^5.4.0" + "hookified": "^1.11.0", + "keyv": "^5.5.0" } }, "node_modules/cacheable/node_modules/keyv": { @@ -6062,19 +6065,19 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", + "@eslint/js": "9.35.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7617,9 +7620,9 @@ } }, "node_modules/hookified": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.11.0.tgz", - "integrity": "sha512-aDdIN3GyU5I6wextPplYdfmWCo+aLmjjVbntmX6HLD5RCi/xKsivYEBhnRD+d9224zFf008ZpLMPlWF0ZodYZw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.0.tgz", + "integrity": "sha512-hMr1Y9TCLshScrBbV2QxJ9BROddxZ12MX9KsCtuGGy/3SmmN5H1PllKerrVlSotur9dlE8hmUKAOSa3WDzsZmQ==", "dev": true, "license": "MIT" }, @@ -13284,9 +13287,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.23.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.23.1.tgz", - "integrity": "sha512-dNvDTsKV1U2YtiUDfe9d2gp902veFeo3ecCWdGlmLm2WFrAV0+L5LoOj/qHSBABQwMsZPJwfC4bf39mQm1S5zw==", + "version": "16.24.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.24.0.tgz", + "integrity": "sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==", "dev": true, "funding": [ { @@ -13313,7 +13316,7 @@ "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.1.3", + "file-entry-cache": "^10.1.4", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", @@ -13347,9 +13350,9 @@ } }, "node_modules/stylelint-config-recess-order": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.2.0.tgz", - "integrity": "sha512-3Y97dhsWkUHFKRLGNLF6LE5JuNB2EVAZKYJ41wBRK4gplYdk7eHhSIwE24hanu0AoNmv5534Djip70pE+y5qkA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.3.0.tgz", + "integrity": "sha512-1LZhQi/D6OljSLRKejMEzbZA8h0AKkJH7p2y+eValc9ltWRGVznjnZsNLVCOwYpKk7GlYMLNVYTc9WpA0W3TYQ==", "dev": true, "license": "ISC", "peerDependencies": { @@ -13428,25 +13431,25 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.3.tgz", - "integrity": "sha512-D+w75Ub8T55yor7fPgN06rkCAUbAYw2vpxJmmjv/GDAcvCnv9g7IvHhIZoxzRZThrXPFI2maeY24pPbtyYU7Lg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz", + "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^6.1.12" + "flat-cache": "^6.1.13" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.12", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.12.tgz", - "integrity": "sha512-U+HqqpZPPXP5d24bWuRzjGqVqUcw64k4nZAbruniDwdRg0H10tvN7H6ku1tjhA4rg5B9GS3siEvwO2qjJJ6f8Q==", + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.13.tgz", + "integrity": "sha512-gmtS2PaUjSPa4zjObEIn4WWliKyZzYljgxODBfxugpK6q6HU9ClXzgCJ+nlcPKY9Bt090ypTOLIFWkV0jbKFjw==", "dev": true, "license": "MIT", "dependencies": { - "cacheable": "^1.10.3", + "cacheable": "^1.10.4", "flatted": "^3.3.3", - "hookified": "^1.10.0" + "hookified": "^1.11.0" } }, "node_modules/stylelint/node_modules/ignore": { diff --git a/package.json b/package.json index b55b96b31..b36b9959a 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "devDependencies": { "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.34.0", + "eslint": "^9.35.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", @@ -146,8 +146,8 @@ "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.23.1", - "stylelint-config-recess-order": "^7.2.0", + "stylelint": "^16.24.0", + "stylelint-config-recess-order": "^7.3.0", "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" } From d94afa9c508db14b2028428e676ee2fac00a1a19 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 19:33:02 -0400 Subject: [PATCH 33/48] convert functions and states --- client/homebrew/pages/editPage/editPage.jsx | 618 +++++++++----------- 1 file changed, 272 insertions(+), 346 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 51196a444..00b88346f 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,449 +1,375 @@ /* eslint-disable max-lines */ -require('./editPage.less'); -const React = require('react'); -const _ = require('lodash'); -const createClass = require('create-react-class'); -import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; +import './editPage.less'; + +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import request from '../../utils/request-middleware.js'; +import Markdown from 'naturalcrit/markdown.js'; + +import _ from 'lodash';; +import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; -import request from '../../utils/request-middleware.js'; const { Meta } = require('vitreum/headtags'); -const Nav = require('naturalcrit/nav/nav.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); +import Nav from 'naturalcrit/nav/nav.jsx'; +import Navbar from '../../navbar/navbar.jsx'; +import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; +import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ErrorNavItem from '../../navbar/error-navitem.jsx'; +import HelpNavItem from '../../navbar/help.navitem.jsx'; +import VaultNavItem from '../../navbar/vault.navitem.jsx'; +import PrintNavItem from '../../navbar/print.navitem.jsx'; +import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; -const NewBrew = require('../../navbar/newbrew.navitem.jsx'); -const HelpNavItem = require('../../navbar/help.navitem.jsx'); -const PrintNavItem = require('../../navbar/print.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const Account = require('../../navbar/account.navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const VaultNavItem = require('../../navbar/vault.navitem.jsx'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -const SplitPane = require('client/components/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import LockNotification from './lockNotification/lockNotification.jsx'; -const LockNotification = require('./lockNotification/lockNotification.jsx'); - -import Markdown from 'naturalcrit/markdown.js'; - -const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); -const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); +import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; -const googleDriveIcon = require('../../googleDrive.svg'); +import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const EditPage = createClass({ - displayName : 'EditPage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW_LOAD - }; - }, +const EditPage = (props) => { + props = { + brew: DEFAULT_BREW_LOAD, + ...props + }; + const editor = useRef(null); + const savedBrew = useRef(_.cloneDeep(props.brew)); + const warningTimer = useRef(null); - getInitialState : function() { - return { - brew : this.props.brew, - isSaving : false, - unsavedChanges : false, - alertTrashedGoogleBrew : this.props.brew.trashed, - alertLoginToTransfer : false, - saveGoogle : this.props.brew.googleId ? true : false, - confirmGoogleTransfer : false, - error : null, - htmlErrors : Markdown.validate(this.props.brew.text), - url : '', - autoSave : true, - autoSaveWarning : false, - unsavedTime : new Date(), - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - displayLockMessage : this.props.brew.lock || false, - themeBundle : {} - }; - }, + const [currentBrew , setCurrentBrew ] = useState(props.brew); + const [isSaving , setIsSaving ] = useState(false); + const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); + const [error , setError ] = useState(null); + const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); + const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); + const [themeBundle , setThemeBundle ] = useState({}); + const [unsavedChanges , setUnsavedChanges ] = useState(false); + const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed); + const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); + const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); + const [url , setUrl ] = useState(''); + const [autoSave , setAutoSave ] = useState(true); + const [autoSaveWarning , setAutoSaveWarning ] = useState(false); + const [unsavedTime , setUnsavedTime ] = useState(new Date()); + const [displayLockMessage , setDisplayLockMessage ] = useState(props.brew.lock || false); - editor : React.createRef(null), - savedBrew : null, + const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); - componentDidMount : function(){ - this.setState({ - url : window.location.href - }); + useEffect(() => { + setUrl(window.location.href); - this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy + const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); + setAutoSave(autoSavePref); + setAutoSaveWarning(!autoSavePref) + setHTMLErrors(Markdown.validate(currentBrew.text)); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{ - if(this.state.autoSave){ - this.trySave(); - } else { - this.setState({ autoSaveWarning: true }); - } - }); - - window.onbeforeunload = ()=>{ - if(this.state.isSaving || this.state.unsavedChanges){ + document.addEventListener('keydown', handleControlKeys); + window.onbeforeunload = () => { + if (isSaving || unsavedChanges) { return 'You have unsaved changes!'; } }; - this.setState((prevState)=>({ - htmlErrors : Markdown.validate(prevState.brew.text) - })); + return () => { + document.removeEventListener('keydown', handleControlKeys); + window.onbeforeunload = null; + }; + }, []); - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); + useEffect(() => { + const hasChange = !_.isEqual(currentBrew, savedBrew.current); + setUnsavedChanges(hasChange); + }, [currentBrew]); - document.addEventListener('keydown', this.handleControlKeys); - }, - componentWillUnmount : function() { - window.onbeforeunload = function(){}; - document.removeEventListener('keydown', this.handleControlKeys); - }, - componentDidUpdate : function(){ - const hasChange = this.hasChanges(); - if(this.state.unsavedChanges != hasChange){ - this.setState({ - unsavedChanges : hasChange - }); - } - }, - - handleControlKeys : function(e){ - if(!(e.ctrlKey || e.metaKey)) return; + const handleControlKeys = (e) => { + if (!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; - if(e.keyCode == S_KEY) this.trySave(true); - if(e.keyCode == P_KEY) printCurrentBrew(); - if(e.keyCode == P_KEY || e.keyCode == S_KEY){ + if (e.keyCode === S_KEY) trySave(true); + if (e.keyCode === P_KEY) printCurrentBrew(); + if (e.keyCode === S_KEY || e.keyCode === P_KEY) { e.stopPropagation(); e.preventDefault(); } - }, + }; - handleSplitMove : function(){ - this.editor.current.update(); - }, + const handleSplitMove = () => { + editor.current?.update(); + }; - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, + const handleEditorViewPageChange = (pageNumber) => { + setCurrentEditorViewPageNum(pageNumber); + }; - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + const handleEditorCursorPageChange = (pageNumber) => { + setCurrentEditorCursorPageNum(pageNumber); + }; - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: pageNumber }); - }, + const handleBrewRendererPageChange = (pageNumber) => { + setCurrentBrewRendererPageNum(pageNumber); + }; - handleTextChange : function(text){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(text); + const handleTextChange = (text) => { + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(text); - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); + if (autoSave) debounceSave(); + }; - handleSnipChange : function(snippet){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + const handleStyleChange = (style) => { + setCurrentBrew(prevBrew => ({ ...prevBrew, style })); + if (autoSave) debounceSave(); + }; - this.setState((prevState)=>({ - brew : { ...prevState.brew, snippets: snippet }, - unsavedChanges : true, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + const handleSnipChange = (snippet)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(snippet); - handleStyleChange : function(style){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); + if (autoSave) debounceSave(); + }; - handleMetaChange : function(metadata, field=undefined){ - if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme); + const handleMetaChange = (metadata, field = undefined) => { + if (field === 'theme' || field === 'renderer') + fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - ...metadata - } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setCurrentBrew(prev => ({ ...prev, ...metadata })); + if (autoSave) debounceSave(); + }; - hasChanges : function(){ - return !_.isEqual(this.state.brew, this.savedBrew); - }, - - updateBrew : function(newData){ - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - style : newData.style, - text : newData.text, - snippets : newData.snippets - } + const updateBrew = (newData) => + setCurrentBrew((prevBrew) => ({ + ...prevBrew, + style : newData.style, + text : newData.text, + snippets : newData.snippets })); - }, - - trySave : function(immediate=false){ - if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); - if(this.state.isSaving) - return; - - if(immediate) { - this.debounceSave(); - this.debounceSave.flush(); - return; - } - if(this.hasChanges()) - this.debounceSave(); - else - this.debounceSave.cancel(); - }, + const trySave = (immediate = false) => { + if (!debounceSave.current) return; + if (isSaving) return; - handleGoogleClick : function(){ - if(!global.account?.googleId) { - this.setState({ - alertLoginToTransfer : true - }); + const hasChange = !_.isEqual(currentBrew, savedBrew.current); + + if (immediate) { + debounceSave.current(); + debounceSave.current.flush?.(); return; } - this.setState((prevState)=>({ - confirmGoogleTransfer : !prevState.confirmGoogleTransfer - })); - this.setState({ - error : null - }); - }, - closeAlerts : function(event){ - event.stopPropagation(); //Only handle click once so alert doesn't reopen - this.setState({ - alertTrashedGoogleBrew : false, - alertLoginToTransfer : false, - confirmGoogleTransfer : false - }); - }, + if (hasChange) { + debounceSave.current(); + } else { + debounceSave.current.cancel?.(); + } + }; - toggleGoogleStorage : function(){ - this.setState((prevState)=>({ - saveGoogle : !prevState.saveGoogle, - error : null - }), ()=>this.trySave(true)); - }, + const handleGoogleClick = () => { + if (!global.account?.googleId) { + setAlertLoginToTransfer(true); + return; + } - save : async function(){ - if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); + setConfirmGoogleTransfer((prev) => !prev); + setError(null); + }; - const brewState = this.state.brew; // freeze the current state - const preSaveSnapshot = { ...brewState }; + const closeAlerts = (e) => { + e.stopPropagation(); //Only handle click once so alert doesn't reopen + setAlertTrashedGoogleBrew(false); + setAlertLoginToTransfer(false); + setConfirmGoogleTransfer(false); + }; - this.setState((prevState)=>({ - isSaving : true, - error : null, - htmlErrors : Markdown.validate(prevState.brew.text) - })); + const toggleGoogleStorage = () => { + setSaveGoogle((prev) => !prev); + setError(null); + trySave(true); + }; - await updateHistory(this.state.brew).catch(console.error); + const save = async () => { + debounceSave.current?.cancel?.(); + + setIsSaving(true); + setError(null); + setHTMLErrors(Markdown.validate(currentBrew.text)); + + await updateHistory(currentBrew).catch(console.error); await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server - const brew = { ...brewState }; - brew.text = brew.text.normalize('NFC'); - this.savedBrew.text = this.savedBrew.text.normalize('NFC'); - brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; - brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text))); - brew.hash = await md5(this.savedBrew.text); - //brew.text = undefined; - Temporary parallel path - brew.textBin = undefined; + const brewToSave = { + ...currentBrew, + text : currentBrew.text.normalize('NFC'), + pageCount: ((currentBrew.renderer === 'legacy' ? currentBrew.text.match(/\\page/g) : currentBrew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(currentBrew.text.normalize('NFC')))), + hash : await md5(savedBrew.current.text), + textBin : undefined + }; - const compressedBrew = gzipSync(strToU8(JSON.stringify(brew))); + const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); + const transfer = saveGoogle === _.isNil(currentBrew.googleId); + const params = transfer ? `?${saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; - const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); - const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; const res = await request - .put(`/api/update/${brew.editId}${params}`) + .put(`/api/update/${brewToSave.editId}${params}`) .set('Content-Encoding', 'gzip') .set('Content-Type', 'application/json') .send(compressedBrew) .catch((err)=>{ - console.log('Error Updating Local Brew'); - this.setState({ error: err }); + console.error('Error Updating Local Brew'); + setError(err); }); if(!res) return; - this.savedBrew = { - ...preSaveSnapshot, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version + const { googleId, editId, shareId, version } = res.body; + + savedBrew.current = { + ...currentBrew, + googleId: googleId ?? null, + editId, + shareId, + version }; - this.setState((prevState) => ({ - brew: { - ...prevState.brew, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version - }, - isSaving : false, - unsavedTime : new Date() - }), ()=>{ - this.setState({ unsavedChanges : this.hasChanges() }); - }); + setCurrentBrew(prev => ({ + ...prev, + googleId: googleId ?? null, + editId, + shareId, + version + })); - history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); - }, + setIsSaving(false); + setUnsavedTime(new Date()); + setUnsavedChanges(!_.isEqual(currentBrew, savedBrew.current)); - renderGoogleDriveIcon : function(){ - return - Google Drive icon + history.replaceState(null, null, `/edit/${editId}`); + }; - {this.state.confirmGoogleTransfer && -
- { this.state.saveGoogle - ? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?` - : `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?` - } + const renderGoogleDriveIcon = () => ( + + Google Drive icon + + {confirmGoogleTransfer && ( +
+ {saveGoogle + ? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?' + : 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
-
- Yes -
-
- No -
+
Yes
+
No
- } + )} - {this.state.alertLoginToTransfer && -
- You must be signed in to a Google account to transfer - between the homebrewery and Google Drive! - -
- Sign In -
+ {alertLoginToTransfer && ( +
- } + )} - {this.state.alertTrashedGoogleBrew && -
- This brew is currently in your Trash folder on Google Drive!
If you want to keep it, make sure to move it before it is deleted permanently!
-
- OK -
+ {alertTrashedGoogleBrew && ( +
+ This brew is currently in your Trash folder on Google Drive!
+ If you want to keep it, make sure to move it before it is deleted permanently!
+
OK
- } - ; - }, - - renderSaveButton : function(){ + )} + + ); + const renderSaveButton = () => { // #1 - Currently saving, show SAVING - if(this.state.isSaving){ + if (isSaving) return saving...; - } // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(this.state.unsavedChanges && this.state.autoSaveWarning){ - this.setAutosaveWarning(); - const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60); - const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + if (unsavedChanges && autoSaveWarning) { + setAutosaveWarning(); + const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? 'Autosave is OFF.' + : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; return - Reminder... -
- {text} -
-
; + Reminder... +
{text}
+ } // #3 - Unsaved changes exist, click to save, show SAVE NOW - // Use trySave(true) instead of save() to use debounced save function - if(this.state.unsavedChanges){ - return this.trySave(true)} color='blue' icon='fas fa-save'>Save Now; - } + if (unsavedChanges) + return trySave(true)} color='blue' icon='fas fa-save'>Save Now + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(this.state.autoSave){ + if (autoSave) return auto-saved.; - } + // DEFAULT - No unsaved changes, show SAVED return saved.; - }, + }; - handleAutoSave : function(){ - if(this.warningTimer) clearTimeout(this.warningTimer); - this.setState((prevState)=>({ - autoSave : !prevState.autoSave, - autoSaveWarning : prevState.autoSave - }), ()=>{ - localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave)); - }); - }, + const handleAutoSave = () => { + if (warningTimer.current) clearTimeout(warningTimer.current); + localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); + setAutoSave(!autoSave); + setAutoSaveWarning(false); + }; - setAutosaveWarning : function(){ - setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display - this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings - this.warningTimer; - }, + const resetAutosaveWarning = () => { + setTimeout(setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds + warningTimer.current = setTimeout(setAutoSaveWarning(true), 900000); // 15 minutes between unsaved changes warnings + }; - errorReported : function(error) { - this.setState({ - error - }); - }, + const renderAutoSaveButton = () => ( + + Autosave + + ); - renderAutoSaveButton : function(){ - return - Autosave - ; - }, + const processShareId = () => ( + currentBrew.googleId && !currentBrew.stubbed + ? currentBrew.googleId + currentBrew.shareId + : currentBrew.shareId + ); - processShareId : function() { - return this.state.brew.googleId && !this.state.brew.stubbed ? - this.state.brew.googleId + this.state.brew.shareId : - this.state.brew.shareId; - }, - - getRedditLink : function(){ - - const shareLink = this.processShareId(); - const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : ''; - const title = `${this.props.brew.title} ${systems}`; + const getRedditLink = () => { + const shareLink = processShareId(); + const systems = currentBrew.systems.length > 0 ? ` [${currentBrew.systems.join(' - ')}]` : ''; + const title = `${currentBrew.title} ${systems}`; const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. -**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; + **[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; - }, + }; - clearError : function(){ - setState({ - error : null, - isSaving : false - }) - }, + const clearError = () => { + setError(null); + setIsSaving(false); + }; renderNavbar : function(){ const shareLink = this.processShareId(); @@ -502,7 +428,7 @@ const EditPage = createClass({ onStyleChange={this.handleStyleChange} onSnipChange={this.handleSnipChange} onMetaChange={this.handleMetaChange} - reportError={this.errorReported} + reportError={setError} renderer={this.state.brew.renderer} userThemes={this.props.userThemes} themeBundle={this.state.themeBundle} From 597ce7cb48c3470474ed2335c9f028f61c256713 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:05:47 -0400 Subject: [PATCH 34/48] Convert renderNavBar and render --- client/homebrew/pages/editPage/editPage.jsx | 106 ++++++++++---------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 00b88346f..20ef1f006 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -42,7 +42,7 @@ const EditPage = (props) => { brew: DEFAULT_BREW_LOAD, ...props }; - const editor = useRef(null); + const editorRef = useRef(null); const savedBrew = useRef(_.cloneDeep(props.brew)); const warningTimer = useRef(null); @@ -107,7 +107,7 @@ const EditPage = (props) => { }; const handleSplitMove = () => { - editor.current?.update(); + editorRef.current?.update(); }; const handleEditorViewPageChange = (pageNumber) => { @@ -371,24 +371,23 @@ const EditPage = (props) => { setIsSaving(false); }; - renderNavbar : function(){ - const shareLink = this.processShareId(); + const renderNavbar = ()=>{ + const shareLink = processShareId(); return - {this.state.brew.title} + {currentBrew.title} - {this.renderGoogleDriveIcon()} - {this.state.error ? - : - - {this.renderSaveButton()} - {this.renderAutoSaveButton()} - - } - + {renderGoogleDriveIcon()} + {error + ? + : + {renderSaveButton()} + {renderAutoSaveButton()} + } + @@ -400,63 +399,64 @@ const EditPage = (props) => { {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}> copy url - + post to reddit - - + + - ; - }, + }; - render : function(){ - return
+ return ( +
- {this.renderNavbar()} - {this.props.brew.lock && } + {renderNavbar()} + + {currentBrew.lock && } +
- +
-
; - } -}); +
+ ); +}; module.exports = EditPage; From a75364c7f6fa335ba903daa2b3f6edb5d1f3fc06 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:06:16 -0400 Subject: [PATCH 35/48] remove unused displayLockMessage state --- client/homebrew/pages/editPage/editPage.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 20ef1f006..f34d6dd9b 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -63,7 +63,6 @@ const EditPage = (props) => { const [autoSave , setAutoSave ] = useState(true); const [autoSaveWarning , setAutoSaveWarning ] = useState(false); const [unsavedTime , setUnsavedTime ] = useState(new Date()); - const [displayLockMessage , setDisplayLockMessage ] = useState(props.brew.lock || false); const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); From 883f59ff0dc835516f018415cfd5041b5e557c1b Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:13:21 -0400 Subject: [PATCH 36/48] rename `autosave` state to `autoSaveEnabled` --- client/homebrew/pages/editPage/editPage.jsx | 24 +++++++++++---------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index f34d6dd9b..002b9f468 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -60,7 +60,7 @@ const EditPage = (props) => { const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); const [url , setUrl ] = useState(''); - const [autoSave , setAutoSave ] = useState(true); + const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); const [autoSaveWarning , setAutoSaveWarning ] = useState(false); const [unsavedTime , setUnsavedTime ] = useState(new Date()); @@ -70,7 +70,7 @@ const EditPage = (props) => { setUrl(window.location.href); const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); - setAutoSave(autoSavePref); + setAutoSaveEnabled(autoSavePref); setAutoSaveWarning(!autoSavePref) setHTMLErrors(Markdown.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); @@ -91,6 +91,8 @@ const EditPage = (props) => { useEffect(() => { const hasChange = !_.isEqual(currentBrew, savedBrew.current); setUnsavedChanges(hasChange); + + if(autoSaveEnabled) save(); }, [currentBrew]); const handleControlKeys = (e) => { @@ -128,12 +130,12 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const handleStyleChange = (style) => { setCurrentBrew(prevBrew => ({ ...prevBrew, style })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const handleSnipChange = (snippet)=>{ @@ -143,7 +145,7 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const handleMetaChange = (metadata, field = undefined) => { @@ -151,7 +153,7 @@ const EditPage = (props) => { fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); setCurrentBrew(prev => ({ ...prev, ...metadata })); - if (autoSave) debounceSave(); + if (autoSaveEnabled) debounceSave(); }; const updateBrew = (newData) => @@ -306,7 +308,7 @@ const EditPage = (props) => { // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING if (unsavedChanges && autoSaveWarning) { - setAutosaveWarning(); + resetAutoSaveWarning(); const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 ? 'Autosave is OFF.' @@ -323,7 +325,7 @@ const EditPage = (props) => { return trySave(true)} color='blue' icon='fas fa-save'>Save Now // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if (autoSave) + if (autoSaveEnabled) return auto-saved.; // DEFAULT - No unsaved changes, show SAVED @@ -333,18 +335,18 @@ const EditPage = (props) => { const handleAutoSave = () => { if (warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); - setAutoSave(!autoSave); + setAutoSaveEnabled(!autoSaveEnabled); setAutoSaveWarning(false); }; - const resetAutosaveWarning = () => { + const resetAutoSaveWarning = () => { setTimeout(setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds warningTimer.current = setTimeout(setAutoSaveWarning(true), 900000); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( - Autosave + Autosave ); From 90a81237ec8fd5c6ddddde0f56f0f56691fe7c41 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 8 Sep 2025 23:18:25 -0400 Subject: [PATCH 37/48] rename handleAutoSave to toggleAutoSave --- client/homebrew/pages/editPage/editPage.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 002b9f468..95d367522 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -332,7 +332,7 @@ const EditPage = (props) => { return saved.; }; - const handleAutoSave = () => { + const toggleAutoSave = () => { if (warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); setAutoSaveEnabled(!autoSaveEnabled); @@ -345,7 +345,7 @@ const EditPage = (props) => { }; const renderAutoSaveButton = () => ( - + Autosave ); From 90f6e7ec372bd3bbdaaacecc2586dd1b1cf0ae97 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 01:57:13 -0400 Subject: [PATCH 38/48] Make autosaving work debouncing does not play nice with functional component. Any debounced function gets locked in as the original state, meaning we keep saving the original document and overwriting the current document when a save fires. Must pass in the parameters instead of pulling directly from state to work properly. --- client/homebrew/pages/editPage/editPage.jsx | 64 +++++++++------------ 1 file changed, 26 insertions(+), 38 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 95d367522..bfb268496 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import './editPage.less'; -import React, { useState, useEffect, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; @@ -35,16 +35,13 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; -const SAVE_TIMEOUT = 10000; +const SAVE_TIMEOUT = 5000; const EditPage = (props) => { props = { brew: DEFAULT_BREW_LOAD, ...props }; - const editorRef = useRef(null); - const savedBrew = useRef(_.cloneDeep(props.brew)); - const warningTimer = useRef(null); const [currentBrew , setCurrentBrew ] = useState(props.brew); const [isSaving , setIsSaving ] = useState(false); @@ -64,7 +61,10 @@ const EditPage = (props) => { const [autoSaveWarning , setAutoSaveWarning ] = useState(false); const [unsavedTime , setUnsavedTime ] = useState(new Date()); - const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); + const editorRef = useRef(null); + const savedBrew = useRef(_.cloneDeep(props.brew)); + const warningTimer = useRef(null); + const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); useEffect(() => { setUrl(window.location.href); @@ -92,7 +92,7 @@ const EditPage = (props) => { const hasChange = !_.isEqual(currentBrew, savedBrew.current); setUnsavedChanges(hasChange); - if(autoSaveEnabled) save(); + if(hasChange && autoSaveEnabled) trySave(); }, [currentBrew]); const handleControlKeys = (e) => { @@ -130,12 +130,10 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); - if (autoSaveEnabled) debounceSave(); }; const handleStyleChange = (style) => { setCurrentBrew(prevBrew => ({ ...prevBrew, style })); - if (autoSaveEnabled) debounceSave(); }; const handleSnipChange = (snippet)=>{ @@ -145,7 +143,6 @@ const EditPage = (props) => { setHTMLErrors(HTMLErrors); setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); - if (autoSaveEnabled) debounceSave(); }; const handleMetaChange = (metadata, field = undefined) => { @@ -153,7 +150,6 @@ const EditPage = (props) => { fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); setCurrentBrew(prev => ({ ...prev, ...metadata })); - if (autoSaveEnabled) debounceSave(); }; const updateBrew = (newData) => @@ -165,22 +161,21 @@ const EditPage = (props) => { })); const trySave = (immediate = false) => { - if (!debounceSave.current) return; + //debounceSave = _.debounce(save, SAVE_TIMEOUT); if (isSaving) return; const hasChange = !_.isEqual(currentBrew, savedBrew.current); if (immediate) { - debounceSave.current(); - debounceSave.current.flush?.(); + debounceSave(currentBrew, saveGoogle); + debounceSave.flush?.(); return; } - if (hasChange) { - debounceSave.current(); - } else { - debounceSave.current.cancel?.(); - } + if (hasChange) + debounceSave(currentBrew, saveGoogle); + else + debounceSave.cancel?.(); }; const handleGoogleClick = () => { @@ -206,29 +201,29 @@ const EditPage = (props) => { trySave(true); }; - const save = async () => { - debounceSave.current?.cancel?.(); + const save = async (brew, saveToGoogle) => { + debounceSave?.cancel?.(); setIsSaving(true); setError(null); - setHTMLErrors(Markdown.validate(currentBrew.text)); + setHTMLErrors(Markdown.validate(brew.text)); - await updateHistory(currentBrew).catch(console.error); + await updateHistory(brew).catch(console.error); await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server const brewToSave = { - ...currentBrew, - text : currentBrew.text.normalize('NFC'), - pageCount: ((currentBrew.renderer === 'legacy' ? currentBrew.text.match(/\\page/g) : currentBrew.text.match(/^\\page$/gm)) || []).length + 1, - patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(currentBrew.text.normalize('NFC')))), + ...brew, + text : brew.text.normalize('NFC'), + pageCount: ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), hash : await md5(savedBrew.current.text), textBin : undefined }; const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); - const transfer = saveGoogle === _.isNil(currentBrew.googleId); - const params = transfer ? `?${saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; + const transfer = saveToGoogle === _.isNil(brew.googleId); + const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; const res = await request .put(`/api/update/${brewToSave.editId}${params}`) @@ -244,24 +239,17 @@ const EditPage = (props) => { const { googleId, editId, shareId, version } = res.body; savedBrew.current = { - ...currentBrew, + ...brew, googleId: googleId ?? null, editId, shareId, version }; - setCurrentBrew(prev => ({ - ...prev, - googleId: googleId ?? null, - editId, - shareId, - version - })); + setCurrentBrew(savedBrew.current); setIsSaving(false); setUnsavedTime(new Date()); - setUnsavedChanges(!_.isEqual(currentBrew, savedBrew.current)); history.replaceState(null, null, `/edit/${editId}`); }; From 8706f91b586345a8b5e257a2695271dcf0af4c2e Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 08:37:17 -0400 Subject: [PATCH 39/48] Fis autosaveWarning --- client/homebrew/pages/editPage/editPage.jsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index bfb268496..3c4039fad 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -35,7 +35,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; -const SAVE_TIMEOUT = 5000; +const SAVE_TIMEOUT = 10000; const EditPage = (props) => { props = { @@ -58,7 +58,7 @@ const EditPage = (props) => { const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); const [url , setUrl ] = useState(''); const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); - const [autoSaveWarning , setAutoSaveWarning ] = useState(false); + const [autoSaveWarning , setAutoSaveWarning ] = useState(true); const [unsavedTime , setUnsavedTime ] = useState(new Date()); const editorRef = useRef(null); @@ -297,6 +297,7 @@ const EditPage = (props) => { // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING if (unsavedChanges && autoSaveWarning) { resetAutoSaveWarning(); + console.log("just set the timer") const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 ? 'Autosave is OFF.' @@ -323,13 +324,13 @@ const EditPage = (props) => { const toggleAutoSave = () => { if (warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); + setAutoSaveWarning(autoSaveWarning); setAutoSaveEnabled(!autoSaveEnabled); - setAutoSaveWarning(false); }; const resetAutoSaveWarning = () => { - setTimeout(setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds - warningTimer.current = setTimeout(setAutoSaveWarning(true), 900000); // 15 minutes between unsaved changes warnings + setTimeout(()=>setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds + warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 3000); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( From 6f2c3975748c7a67f79c6a4df06d0e407a4a37ed Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 20:47:09 -0400 Subject: [PATCH 40/48] Restore autosave warning to 15 minutes --- client/homebrew/pages/editPage/editPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 3c4039fad..d22aeadb8 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -330,7 +330,7 @@ const EditPage = (props) => { const resetAutoSaveWarning = () => { setTimeout(()=>setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds - warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 3000); // 15 minutes between unsaved changes warnings + warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 90000); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( From 8a0f350c475bf78d41890e7b75afcc1b87a9d21c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:19:43 -0400 Subject: [PATCH 41/48] Fix mutating HTMLErrors directly instead of setState --- client/homebrew/pages/editPage/editPage.jsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d22aeadb8..b748efa87 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -126,9 +126,7 @@ const EditPage = (props) => { const handleTextChange = (text) => { //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) - HTMLErrors = Markdown.validate(text); - - setHTMLErrors(HTMLErrors); + setHTMLErrors(Markdown.validate(text)); setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); }; @@ -139,9 +137,7 @@ const EditPage = (props) => { const handleSnipChange = (snippet)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) - HTMLErrors = Markdown.validate(snippet); - - setHTMLErrors(HTMLErrors); + setHTMLErrors(Markdown.validate(snippet)); setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); }; From 1044aa74b0127c0b49a627205be074d4a365ec9a Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:27:58 -0400 Subject: [PATCH 42/48] Cleanup --- client/homebrew/pages/editPage/editPage.jsx | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index b748efa87..161431663 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -5,10 +5,10 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; -import _ from 'lodash';; -import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; +import _ from 'lodash';; +import {makePatches, stringifyPatches} from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; const { Meta } = require('vitreum/headtags'); @@ -28,14 +28,16 @@ import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; import LockNotification from './lockNotification/lockNotification.jsx'; -import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; -import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; +import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; +const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const EditPage = (props) => { props = { @@ -77,9 +79,8 @@ const EditPage = (props) => { document.addEventListener('keydown', handleControlKeys); window.onbeforeunload = () => { - if (isSaving || unsavedChanges) { + if (isSaving || unsavedChanges) return 'You have unsaved changes!'; - } }; return () => { @@ -293,7 +294,6 @@ const EditPage = (props) => { // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING if (unsavedChanges && autoSaveWarning) { resetAutoSaveWarning(); - console.log("just set the timer") const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 ? 'Autosave is OFF.' @@ -325,8 +325,8 @@ const EditPage = (props) => { }; const resetAutoSaveWarning = () => { - setTimeout(()=>setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds - warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), 90000); // 15 minutes between unsaved changes warnings + setTimeout(()=>setAutoSaveWarning(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds + warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved changes warnings }; const renderAutoSaveButton = () => ( From 95a1d74644a76b92cc3482e9f860defc34b7868c Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:35:55 -0400 Subject: [PATCH 43/48] Linting --- client/homebrew/pages/editPage/editPage.jsx | 141 ++++++++++---------- 1 file changed, 69 insertions(+), 72 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 161431663..925a36e86 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -5,12 +5,11 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; import request from '../../utils/request-middleware.js'; import Markdown from 'naturalcrit/markdown.js'; -import _ from 'lodash';; -import {makePatches, stringifyPatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; - -const { Meta } = require('vitreum/headtags'); +import _ from 'lodash';; +import { makePatches, stringifyPatches } from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; +import { Meta } from 'vitreum/headtags'; import Nav from 'naturalcrit/nav/nav.jsx'; import Navbar from '../../navbar/navbar.jsx'; @@ -39,9 +38,9 @@ const SAVE_TIMEOUT = 10000; const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds -const EditPage = (props) => { +const EditPage = (props)=>{ props = { - brew: DEFAULT_BREW_LOAD, + brew : DEFAULT_BREW_LOAD, ...props }; @@ -64,141 +63,139 @@ const EditPage = (props) => { const [unsavedTime , setUnsavedTime ] = useState(new Date()); const editorRef = useRef(null); - const savedBrew = useRef(_.cloneDeep(props.brew)); - const warningTimer = useRef(null); + const savedBrew = useRef(_.cloneDeep(props.brew)); const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); - useEffect(() => { + useEffect(()=>{ setUrl(window.location.href); const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); setAutoSaveEnabled(autoSavePref); - setAutoSaveWarning(!autoSavePref) + setAutoSaveWarning(!autoSavePref); setHTMLErrors(Markdown.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); document.addEventListener('keydown', handleControlKeys); - window.onbeforeunload = () => { - if (isSaving || unsavedChanges) + window.onbeforeunload = ()=>{ + if(isSaving || unsavedChanges) return 'You have unsaved changes!'; }; - return () => { + return ()=>{ document.removeEventListener('keydown', handleControlKeys); window.onbeforeunload = null; }; }, []); - useEffect(() => { + useEffect(()=>{ const hasChange = !_.isEqual(currentBrew, savedBrew.current); setUnsavedChanges(hasChange); if(hasChange && autoSaveEnabled) trySave(); }, [currentBrew]); - const handleControlKeys = (e) => { - if (!(e.ctrlKey || e.metaKey)) return; + const handleControlKeys = (e)=>{ + if(!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; - if (e.keyCode === S_KEY) trySave(true); - if (e.keyCode === P_KEY) printCurrentBrew(); - if (e.keyCode === S_KEY || e.keyCode === P_KEY) { + if(e.keyCode === S_KEY) trySave(true); + if(e.keyCode === P_KEY) printCurrentBrew(); + if(e.keyCode === S_KEY || e.keyCode === P_KEY) { e.stopPropagation(); e.preventDefault(); } }; - const handleSplitMove = () => { + const handleSplitMove = ()=>{ editorRef.current?.update(); }; - const handleEditorViewPageChange = (pageNumber) => { + const handleEditorViewPageChange = (pageNumber)=>{ setCurrentEditorViewPageNum(pageNumber); }; - const handleEditorCursorPageChange = (pageNumber) => { + const handleEditorCursorPageChange = (pageNumber)=>{ setCurrentEditorCursorPageNum(pageNumber); }; - const handleBrewRendererPageChange = (pageNumber) => { + const handleBrewRendererPageChange = (pageNumber)=>{ setCurrentBrewRendererPageNum(pageNumber); }; - const handleTextChange = (text) => { + const handleTextChange = (text)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) setHTMLErrors(Markdown.validate(text)); - setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); + setCurrentBrew((prevBrew)=>({ ...prevBrew, text })); }; - const handleStyleChange = (style) => { - setCurrentBrew(prevBrew => ({ ...prevBrew, style })); + const handleStyleChange = (style)=>{ + setCurrentBrew((prevBrew)=>({ ...prevBrew, style })); }; const handleSnipChange = (snippet)=>{ //If there are HTML errors, run the validator on every change to give quick feedback if(HTMLErrors.length) setHTMLErrors(Markdown.validate(snippet)); - setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); + setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet })); }; - const handleMetaChange = (metadata, field = undefined) => { - if (field === 'theme' || field === 'renderer') + const handleMetaChange = (metadata, field = undefined)=>{ + if(field === 'theme' || field === 'renderer') fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - setCurrentBrew(prev => ({ ...prev, ...metadata })); + setCurrentBrew((prev)=>({ ...prev, ...metadata })); }; - const updateBrew = (newData) => - setCurrentBrew((prevBrew) => ({ - ...prevBrew, - style : newData.style, - text : newData.text, - snippets : newData.snippets - })); - - const trySave = (immediate = false) => { + const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ + ...prevBrew, + style : newData.style, + text : newData.text, + snippets : newData.snippets + })); + + const trySave = (immediate = false)=>{ //debounceSave = _.debounce(save, SAVE_TIMEOUT); - if (isSaving) return; + if(isSaving) return; const hasChange = !_.isEqual(currentBrew, savedBrew.current); - if (immediate) { + if(immediate) { debounceSave(currentBrew, saveGoogle); debounceSave.flush?.(); return; } - if (hasChange) + if(hasChange) debounceSave(currentBrew, saveGoogle); else debounceSave.cancel?.(); }; - const handleGoogleClick = () => { - if (!global.account?.googleId) { + const handleGoogleClick = ()=>{ + if(!global.account?.googleId) { setAlertLoginToTransfer(true); return; } - setConfirmGoogleTransfer((prev) => !prev); + setConfirmGoogleTransfer((prev)=>!prev); setError(null); }; - const closeAlerts = (e) => { + const closeAlerts = (e)=>{ e.stopPropagation(); //Only handle click once so alert doesn't reopen setAlertTrashedGoogleBrew(false); setAlertLoginToTransfer(false); setConfirmGoogleTransfer(false); }; - const toggleGoogleStorage = () => { - setSaveGoogle((prev) => !prev); + const toggleGoogleStorage = ()=>{ + setSaveGoogle((prev)=>!prev); setError(null); trySave(true); }; - const save = async (brew, saveToGoogle) => { + const save = async (brew, saveToGoogle)=>{ debounceSave?.cancel?.(); setIsSaving(true); @@ -211,11 +208,11 @@ const EditPage = (props) => { //Prepare content to send to server const brewToSave = { ...brew, - text : brew.text.normalize('NFC'), - pageCount: ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, - patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), - hash : await md5(savedBrew.current.text), - textBin : undefined + text : brew.text.normalize('NFC'), + pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), + hash : await md5(savedBrew.current.text), + textBin : undefined }; const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); @@ -237,7 +234,7 @@ const EditPage = (props) => { savedBrew.current = { ...brew, - googleId: googleId ?? null, + googleId : googleId ?? null, editId, shareId, version @@ -251,7 +248,7 @@ const EditPage = (props) => { history.replaceState(null, null, `/edit/${editId}`); }; - const renderGoogleDriveIcon = () => ( + const renderGoogleDriveIcon = ()=>( Google Drive icon @@ -286,13 +283,13 @@ const EditPage = (props) => { ); - const renderSaveButton = () => { + const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING - if (isSaving) + if(isSaving) return saving...; // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if (unsavedChanges && autoSaveWarning) { + if(unsavedChanges && autoSaveWarning) { resetAutoSaveWarning(); const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); const text = elapsedTime === 0 @@ -306,42 +303,42 @@ const EditPage = (props) => { } // #3 - Unsaved changes exist, click to save, show SAVE NOW - if (unsavedChanges) - return trySave(true)} color='blue' icon='fas fa-save'>Save Now + if(unsavedChanges) + return trySave(true)} color='blue' icon='fas fa-save'>Save Now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if (autoSaveEnabled) + if(autoSaveEnabled) return auto-saved.; // DEFAULT - No unsaved changes, show SAVED return saved.; }; - const toggleAutoSave = () => { - if (warningTimer.current) clearTimeout(warningTimer.current); + const toggleAutoSave = ()=>{ + if(warningTimer.current) clearTimeout(warningTimer.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); setAutoSaveWarning(autoSaveWarning); setAutoSaveEnabled(!autoSaveEnabled); }; - const resetAutoSaveWarning = () => { + const resetAutoSaveWarning = ()=>{ setTimeout(()=>setAutoSaveWarning(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved changes warnings }; - const renderAutoSaveButton = () => ( + const renderAutoSaveButton = ()=>( Autosave ); - const processShareId = () => ( + const processShareId = ()=>( currentBrew.googleId && !currentBrew.stubbed ? currentBrew.googleId + currentBrew.shareId : currentBrew.shareId ); - const getRedditLink = () => { + const getRedditLink = ()=>{ const shareLink = processShareId(); const systems = currentBrew.systems.length > 0 ? ` [${currentBrew.systems.join(' - ')}]` : ''; const title = `${currentBrew.title} ${systems}`; @@ -352,7 +349,7 @@ const EditPage = (props) => { return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; }; - const clearError = () => { + const clearError = ()=>{ setError(null); setIsSaving(false); }; From c2e6150edfe10ff00c274bed31f26d8403a15dde Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Tue, 9 Sep 2025 22:39:11 -0400 Subject: [PATCH 44/48] Fix mistaken delete --- client/homebrew/pages/editPage/editPage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 925a36e86..49512d66c 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -64,6 +64,7 @@ const EditPage = (props)=>{ const editorRef = useRef(null); const savedBrew = useRef(_.cloneDeep(props.brew)); + const warningTimer = useRef(null); const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); useEffect(()=>{ From 31a8101df7adf80fafbb0439f2ec85b4d7dae4ee Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sat, 13 Sep 2025 19:37:59 -0400 Subject: [PATCH 45/48] Move "share" dropdown to own component --- client/homebrew/navbar/share.navitem.jsx | 35 +++++++++++++++++++++ client/homebrew/pages/editPage/editPage.jsx | 35 ++------------------- 2 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 client/homebrew/navbar/share.navitem.jsx diff --git a/client/homebrew/navbar/share.navitem.jsx b/client/homebrew/navbar/share.navitem.jsx new file mode 100644 index 000000000..a08ac6878 --- /dev/null +++ b/client/homebrew/navbar/share.navitem.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import dedent from 'dedent-tabs'; +import Nav from 'naturalcrit/nav/nav.jsx'; + + const getShareId = (brew)=>( + brew.googleId && !brew.stubbed + ? brew.googleId + brew.shareId + : brew.shareId + ); + + const getRedditLink = (brew)=>{ + const text = dedent` + Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. + + **[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`; + + return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`; + }; + +export default ({brew}) => ( + + + share + + + view + + {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}> + copy url + + + post to reddit + + +); diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 49512d66c..25e2b152a 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -15,6 +15,7 @@ import Nav from 'naturalcrit/nav/nav.jsx'; import Navbar from '../../navbar/navbar.jsx'; import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ShareNavItem from '../../navbar/share.navitem.jsx'; import ErrorNavItem from '../../navbar/error-navitem.jsx'; import HelpNavItem from '../../navbar/help.navitem.jsx'; import VaultNavItem from '../../navbar/vault.navitem.jsx'; @@ -333,31 +334,12 @@ const EditPage = (props)=>{ ); - const processShareId = ()=>( - currentBrew.googleId && !currentBrew.stubbed - ? currentBrew.googleId + currentBrew.shareId - : currentBrew.shareId - ); - - const getRedditLink = ()=>{ - const shareLink = processShareId(); - const systems = currentBrew.systems.length > 0 ? ` [${currentBrew.systems.join(' - ')}]` : ''; - const title = `${currentBrew.title} ${systems}`; - const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. - - **[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; - - return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; - }; - const clearError = ()=>{ setError(null); setIsSaving(false); }; const renderNavbar = ()=>{ - const shareLink = processShareId(); - return {currentBrew.title} @@ -373,20 +355,7 @@ const EditPage = (props)=>{ } - - - share - - - view - - {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}> - copy url - - - post to reddit - - + From 3ec650557ed4ca4425341f9f9673e56be5b9e2de Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 22 Sep 2025 19:49:57 -0400 Subject: [PATCH 46/48] Fix Autosave and unsaved changes warning Use normal setTimeout for autosave instead of _.debounce. Fixes a lot of issues with functional component. Also fix existing bug where multiple "unsaved data" warnings could be queued up if the user keeps typing while the warning is being displayed. --- client/homebrew/pages/editPage/editPage.jsx | 157 ++++++++++---------- 1 file changed, 80 insertions(+), 77 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 25e2b152a..3fd0b8638 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -36,7 +36,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_TIMEOUT = 9000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const EditPage = (props)=>{ @@ -47,6 +47,7 @@ const EditPage = (props)=>{ const [currentBrew , setCurrentBrew ] = useState(props.brew); const [isSaving , setIsSaving ] = useState(false); + const [lastSavedTime , setLastSavedTime ] = useState(new Date()); const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); const [error , setError ] = useState(null); const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); @@ -58,56 +59,56 @@ const EditPage = (props)=>{ const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed); const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); - const [url , setUrl ] = useState(''); const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); - const [autoSaveWarning , setAutoSaveWarning ] = useState(true); - const [unsavedTime , setUnsavedTime ] = useState(new Date()); + const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true); - const editorRef = useRef(null); - const savedBrew = useRef(_.cloneDeep(props.brew)); - const warningTimer = useRef(null); - const debounceSave = useCallback(_.debounce((brew, saveToGoogle)=>save(brew, saveToGoogle), SAVE_TIMEOUT), []); + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); + const saveTimeout = useRef(null); + const warnUnsavedTimeout = useRef(null); + const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew + const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges useEffect(()=>{ - setUrl(window.location.href); - const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); setAutoSaveEnabled(autoSavePref); - setAutoSaveWarning(!autoSavePref); + setWarnUnsavedChanges(!autoSavePref); setHTMLErrors(Markdown.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - document.addEventListener('keydown', handleControlKeys); - window.onbeforeunload = ()=>{ - if(isSaving || unsavedChanges) - return 'You have unsaved changes!'; + const handleControlKeys = (e) => { + if (!(e.ctrlKey || e.metaKey)) return; + if (e.keyCode === 83) trySaveRef.current(true); + if (e.keyCode === 80) printCurrentBrew(); + if ([83, 80].includes(e.keyCode)) { + e.stopPropagation(); + e.preventDefault(); + } }; - return ()=>{ + document.addEventListener('keydown', handleControlKeys); + window.onbeforeunload = ()=>{ + if(unsavedChangesRef.current) + return 'You have unsaved changes!'; + }; + return () => { document.removeEventListener('keydown', handleControlKeys); - window.onbeforeunload = null; + window.onBeforeUnload = null; }; }, []); useEffect(()=>{ - const hasChange = !_.isEqual(currentBrew, savedBrew.current); + trySaveRef.current = trySave; + unsavedChangesRef.current = unsavedChanges; + }); + + useEffect(()=>{ + const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); setUnsavedChanges(hasChange); - if(hasChange && autoSaveEnabled) trySave(); + if(autoSaveEnabled) trySave(false, hasChange); }, [currentBrew]); - const handleControlKeys = (e)=>{ - if(!(e.ctrlKey || e.metaKey)) return; - const S_KEY = 83; - const P_KEY = 80; - if(e.keyCode === S_KEY) trySave(true); - if(e.keyCode === P_KEY) printCurrentBrew(); - if(e.keyCode === S_KEY || e.keyCode === P_KEY) { - e.stopPropagation(); - e.preventDefault(); - } - }; - const handleSplitMove = ()=>{ editorRef.current?.update(); }; @@ -156,22 +157,10 @@ const EditPage = (props)=>{ snippets : newData.snippets })); - const trySave = (immediate = false)=>{ - //debounceSave = _.debounce(save, SAVE_TIMEOUT); - if(isSaving) return; - - const hasChange = !_.isEqual(currentBrew, savedBrew.current); - - if(immediate) { - debounceSave(currentBrew, saveGoogle); - debounceSave.flush?.(); - return; - } - - if(hasChange) - debounceSave(currentBrew, saveGoogle); - else - debounceSave.cancel?.(); + const resetWarnUnsavedTimer = ()=>{ + setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds + clearTimeout(warnUnsavedTimeout.current); + warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings }; const handleGoogleClick = ()=>{ @@ -197,11 +186,26 @@ const EditPage = (props)=>{ trySave(true); }; - const save = async (brew, saveToGoogle)=>{ - debounceSave?.cancel?.(); + const trySave = (immediate = false, hasChanges = true)=>{ + clearTimeout(saveTimeout.current); + if(isSaving) return; + if(!hasChanges && !immediate) return; + const newTimeout = immediate ? 0 : SAVE_TIMEOUT; - setIsSaving(true); - setError(null); + saveTimeout.current = setTimeout(async () => { + setIsSaving(true); + setError(null); + await save(currentBrew, saveGoogle) + .catch((err)=>{ + setError(err); + }); + setIsSaving(false); + setLastSavedTime(new Date()); + if(!autoSaveEnabled) resetWarnUnsavedTimer(); + }, newTimeout); + }; + + const save = async (brew, saveToGoogle)=>{ setHTMLErrors(Markdown.validate(brew.text)); await updateHistory(brew).catch(console.error); @@ -212,9 +216,10 @@ const EditPage = (props)=>{ ...brew, text : brew.text.normalize('NFC'), pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, - patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), - hash : await md5(savedBrew.current.text), - textBin : undefined + patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), + hash : await md5(lastSavedBrew.current.text), + textBin : undefined, + version : lastSavedBrew.current.version }; const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); @@ -232,22 +237,24 @@ const EditPage = (props)=>{ }); if(!res) return; - const { googleId, editId, shareId, version } = res.body; - - savedBrew.current = { - ...brew, - googleId : googleId ?? null, - editId, - shareId, - version + const updatedFields = { + googleId: res.body.googleId ?? null, + editId : res.body.editId, + shareId : res.body.shareId, + version : res.body.version }; - setCurrentBrew(savedBrew.current); + lastSavedBrew.current = { + ...brew, + ...updatedFields + }; - setIsSaving(false); - setUnsavedTime(new Date()); + setCurrentBrew((prevBrew)=>({ + ...prevBrew, + ...updatedFields + })); - history.replaceState(null, null, `/edit/${editId}`); + history.replaceState(null, null, `/edit/${res.body.editId}`); }; const renderGoogleDriveIcon = ()=>( @@ -268,7 +275,7 @@ const EditPage = (props)=>{ {alertLoginToTransfer && (
You must be signed in to a Google account to transfer between the homebrewery and Google Drive! - +
Sign In
Not Now
@@ -291,9 +298,9 @@ const EditPage = (props)=>{ return saving...; // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(unsavedChanges && autoSaveWarning) { - resetAutoSaveWarning(); - const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); + if(unsavedChanges && warnUnsavedChanges) { + resetWarnUnsavedTimer(); + const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); const text = elapsedTime === 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; @@ -317,15 +324,11 @@ const EditPage = (props)=>{ }; const toggleAutoSave = ()=>{ - if(warningTimer.current) clearTimeout(warningTimer.current); + clearTimeout(warnUnsavedTimeout.current); + clearTimeout(saveTimeout.current); localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); - setAutoSaveWarning(autoSaveWarning); setAutoSaveEnabled(!autoSaveEnabled); - }; - - const resetAutoSaveWarning = ()=>{ - setTimeout(()=>setAutoSaveWarning(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds - warningTimer.current = setTimeout(()=>setAutoSaveWarning(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved changes warnings + setWarnUnsavedChanges(autoSaveEnabled); }; const renderAutoSaveButton = ()=>( From f0baa763ecab11090a53b51a3749417a78f7b2cd Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 22 Sep 2025 19:52:42 -0400 Subject: [PATCH 47/48] lint --- client/homebrew/pages/editPage/editPage.jsx | 36 ++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 3fd0b8638..6a87c8d30 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -66,7 +66,7 @@ const EditPage = (props)=>{ const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const saveTimeout = useRef(null); const warnUnsavedTimeout = useRef(null); - const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew + const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges useEffect(()=>{ @@ -76,11 +76,11 @@ const EditPage = (props)=>{ setHTMLErrors(Markdown.validate(currentBrew.text)); fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - const handleControlKeys = (e) => { - if (!(e.ctrlKey || e.metaKey)) return; - if (e.keyCode === 83) trySaveRef.current(true); - if (e.keyCode === 80) printCurrentBrew(); - if ([83, 80].includes(e.keyCode)) { + const handleControlKeys = (e)=>{ + if(!(e.ctrlKey || e.metaKey)) return; + if(e.keyCode === 83) trySaveRef.current(true); + if(e.keyCode === 80) printCurrentBrew(); + if([83, 80].includes(e.keyCode)) { e.stopPropagation(); e.preventDefault(); } @@ -91,7 +91,7 @@ const EditPage = (props)=>{ if(unsavedChangesRef.current) return 'You have unsaved changes!'; }; - return () => { + return ()=>{ document.removeEventListener('keydown', handleControlKeys); window.onBeforeUnload = null; }; @@ -160,7 +160,7 @@ const EditPage = (props)=>{ const resetWarnUnsavedTimer = ()=>{ setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds clearTimeout(warnUnsavedTimeout.current); - warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings + warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings }; const handleGoogleClick = ()=>{ @@ -192,7 +192,7 @@ const EditPage = (props)=>{ if(!hasChanges && !immediate) return; const newTimeout = immediate ? 0 : SAVE_TIMEOUT; - saveTimeout.current = setTimeout(async () => { + saveTimeout.current = setTimeout(async ()=>{ setIsSaving(true); setError(null); await save(currentBrew, saveGoogle) @@ -238,10 +238,10 @@ const EditPage = (props)=>{ if(!res) return; const updatedFields = { - googleId: res.body.googleId ?? null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version + googleId : res.body.googleId ?? null, + editId : res.body.editId, + shareId : res.body.shareId, + version : res.body.version }; lastSavedBrew.current = { @@ -307,8 +307,8 @@ const EditPage = (props)=>{ return Reminder... -
{text}
-
+
{text}
+ ; } // #3 - Unsaved changes exist, click to save, show SAVE NOW @@ -353,9 +353,9 @@ const EditPage = (props)=>{ {error ? : - {renderSaveButton()} - {renderAutoSaveButton()} - } + {renderSaveButton()} + {renderAutoSaveButton()} + } From c5071aa27eeb383ec34374b27a077b0179aa91e8 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 22 Sep 2025 19:55:39 -0400 Subject: [PATCH 48/48] Restore unsaved warning timeout duration to 15 mins --- client/homebrew/pages/editPage/editPage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 6a87c8d30..6c2220ec1 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -36,7 +36,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const UNSAVED_WARNING_TIMEOUT = 9000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const EditPage = (props)=>{