From f17fbaff49d84e6ed4e120df2d14a26b10001f45 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Fri, 7 Feb 2025 21:44:54 +1300 Subject: [PATCH 01/27] Add event-stream route --- server/app.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/server/app.js b/server/app.js index 4dec6b4c4..f586b760c 100644 --- a/server/app.js +++ b/server/app.js @@ -37,6 +37,8 @@ import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import forceSSL from './forcessl.mw.js'; +import EventEmitter from 'events'; + const sanitizeBrew = (brew, accessType)=>{ brew._id = undefined; @@ -352,7 +354,7 @@ app.get('/user/:username', async (req, res, next)=>{ app.put('/api/user/rename', async (req, res)=>{ const { username, newUsername } = req.body; const ownAccount = req.account && (req.account.username == newUsername); - + if(!username || !newUsername) return res.status(400).json({ error: 'Username and newUsername are required.' }); if(!ownAccount) @@ -512,6 +514,27 @@ app.get('/account', asyncHandler(async (req, res, next)=>{ return next(); })); +// Event Stream +const Stream = new EventEmitter; + +// After Stream starts, send initStream event +setTimeout(()=>{ + Stream.emit('push', 'initStream', { time: (new Date).toString() }); +}, 1000); + +app.get('/stream', (req, res)=>{ + res.writeHead(200, { + 'Content-Type' : 'text/event-stream', + 'Cache-Control' : 'no-cache', + 'Connection' : 'keep-alive' + }); + + Stream.on('push', (event, data)=>{ + // console.log('Event:', event, 'Data:', data); + res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); + }); +}); + // Local only if(isLocalEnvironment){ // Login From 4669c5d48922a5adf7cd7e91b3b327ac5fcdd745 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Fri, 7 Feb 2025 21:45:27 +1300 Subject: [PATCH 02/27] Add reload when initStream message is received --- client/homebrew/homebrew.jsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index 415390498..b8e8aa474 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -66,6 +66,18 @@ const Homebrew = createClass({ return {}; }, + componentDidMount : function(){ + const eventSource = new EventSource('/stream'); + + eventSource.addEventListener('message', (evt)=>{ + const messageData = JSON.parse(evt.data); + + if(messageData.eventType == 'initStream'){ + window.location.reload(); + } + }); + }, + render : function (){ return ( From 0a6434fbf347ef9284caf212cf123953ebb3158f Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Fri, 7 Feb 2025 21:46:28 +1300 Subject: [PATCH 03/27] Increase max-lines on app.js --- server/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app.js b/server/app.js index f586b760c..11a109843 100644 --- a/server/app.js +++ b/server/app.js @@ -1,4 +1,4 @@ -/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ +/*eslint max-lines: ["warn", {"max": 750, "skipBlankLines": true, "skipComments": true}]*/ // Set working directory to project root import { dirname } from 'path'; import { fileURLToPath } from 'url'; From 4ce1cf5eece6fd13a3e7a16ee3fa175870775d1f Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 11 Feb 2025 09:58:00 +1300 Subject: [PATCH 04/27] Move EventStream to external module --- server/app.js | 28 ++++++++++++++-------------- server/eventStreamSource.js | 9 +++++++++ 2 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 server/eventStreamSource.js diff --git a/server/app.js b/server/app.js index 11a109843..e06fda2a5 100644 --- a/server/app.js +++ b/server/app.js @@ -37,7 +37,7 @@ import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import forceSSL from './forcessl.mw.js'; -import EventEmitter from 'events'; +import Stream from './eventStreamSource.js'; const sanitizeBrew = (brew, accessType)=>{ @@ -514,27 +514,27 @@ app.get('/account', asyncHandler(async (req, res, next)=>{ return next(); })); -// Event Stream -const Stream = new EventEmitter; - -// After Stream starts, send initStream event -setTimeout(()=>{ - Stream.emit('push', 'initStream', { time: (new Date).toString() }); -}, 1000); - +// Create Event Stream source for pages to listen to app.get('/stream', (req, res)=>{ res.writeHead(200, { - 'Content-Type' : 'text/event-stream', - 'Cache-Control' : 'no-cache', - 'Connection' : 'keep-alive' + 'Content-Type' : 'text/event-stream', + 'Cache-Control' : 'no-cache', + 'Connection' : 'keep-alive', + 'Content-Encoding' : 'none' }); - Stream.on('push', (event, data)=>{ - // console.log('Event:', event, 'Data:', data); + Stream.on('sendUpdate', (event, data)=>{ + console.log('Event:', event, '\nData:', data); res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`); }); }); +// After Stream starts, send initStream event +setTimeout(()=>{ + Stream.emit('sendUpdate', 'initStream', { time: new Date }); +}, 1000); + + // Local only if(isLocalEnvironment){ // Login diff --git a/server/eventStreamSource.js b/server/eventStreamSource.js new file mode 100644 index 000000000..66fdb3354 --- /dev/null +++ b/server/eventStreamSource.js @@ -0,0 +1,9 @@ +import { EventEmitter } from 'events'; + +const Stream = new EventEmitter; + +export default { + emit : function(event) {return Stream.emit(event, ...([...arguments].slice(1)));}, // Arguments doesn't work for arrow functions + on : (event, listener)=>{return Stream.on(event, listener);}, + off : (event, listener)=>{return Stream.off(event, listener);} +}; \ No newline at end of file From 97ebdf7ae679e86b6881e36027a7aa031cb843d1 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Tue, 11 Feb 2025 09:59:14 +1300 Subject: [PATCH 05/27] Add brew update detection to Share Page --- client/homebrew/pages/sharePage/sharePage.jsx | 12 ++++++++++++ server/homebrew.api.js | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index e9c5540a2..a3fdcdf34 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -46,6 +46,18 @@ const SharePage = (props)=>{ brew.theme ); + const eventSource = new EventSource('/stream'); + + eventSource.addEventListener('message', (evt)=>{ + const messageData = JSON.parse(evt.data); + + if(messageData.eventType == 'brewUpdated'){ + if(messageData.shareId == brew.shareId && messageData.version != brew.version) { + console.log(`brew has been updated, viewing ${brew.version}, new version is ${messageData.version}`); + } + } + }); + return ()=>{ document.removeEventListener('keydown', handleControlKeys); }; diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 9a479732e..57815e384 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -16,6 +16,8 @@ const router = express.Router(); import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js'; import Themes from '../themes/themes.json' with { type: 'json' }; +import Stream from './eventStreamSource.js'; + const isStaticTheme = (renderer, themeName)=>{ return Themes[renderer]?.[themeName] !== undefined; }; @@ -398,6 +400,8 @@ const api = { const after = await afterSave(); if(!after) return; + Stream.emit('sendUpdate', 'brewUpdated', { time: new Date, shareId: brew.shareId, version: brew.version }); + res.status(200).send(saved); }, deleteGoogleBrew : async (account, id, editId, res)=>{ From 01a809a5c7f6f87408bcdf923b8ec65764473ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 10:39:41 +0200 Subject: [PATCH 06/27] split pages into their own routes file --- server/app.js | 361 ++-------------------------------------- server/page-routes.js | 377 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 392 insertions(+), 346 deletions(-) create mode 100644 server/page-routes.js diff --git a/server/app.js b/server/app.js index b1e0e1c25..9e04c7b5b 100644 --- a/server/app.js +++ b/server/app.js @@ -16,18 +16,15 @@ import path from 'path'; import fs from 'fs-extra'; import api from './homebrew.api.js'; +import pageRoutes from './page-routes.js'; const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api; import adminApi from './admin.api.js'; import vaultApi from './vault.api.js'; import GoogleActions from './googleActions.js'; import serveCompressedStaticAssets from './static-assets.mv.js'; -import sanitizeFilename from 'sanitize-filename'; import asyncHandler from 'express-async-handler'; import { model as HomebrewModel } from './homebrew.model.js'; -import { DEFAULT_BREW } from './brewDefaults.js'; -import { splitTextStyleAndMetadata } from '../shared/helpers.js'; - //==== Middleware Imports ====// import contentNegotiation from './middleware/content-negotiation.js'; import bodyParser from 'body-parser'; @@ -132,134 +129,25 @@ export default async function createApp(vite) { type : 'website' }; + app.use(pageRoutes({ + defaultMetaTags, + welcomeText, + welcomeTextLegacy, + migrateText, + changelogText, + faqText, + getBrew, + getUsersBrewThemes, + HomebrewModel, + GoogleActions, + sanitizeBrew, + })); + //Robots.txt app.get('/robots.txt', (req, res)=>{ return res.sendFile(`robots.txt`, { root: process.cwd() }); }); - //Home page - app.get('/', (req, res, next)=>{ - req.brew = { - text : welcomeText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'Homepage', - description : 'Homepage' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Home page Legacy - app.get('/legacy', (req, res, next)=>{ - req.brew = { - text : welcomeTextLegacy, - renderer : 'legacy', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'Homepage (Legacy)', - description : 'Homepage' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Legacy/Other Document -> v3 Migration Guide - app.get('/migrate', (req, res, next)=>{ - req.brew = { - text : migrateText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'v3 Migration Guide', - description : 'A brief guide to converting Legacy documents to the v3 renderer.' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Changelog page - app.get('/changelog', async (req, res, next)=>{ - req.brew = { - title : 'Changelog', - text : changelogText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'Changelog', - description : 'Development changelog.' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //FAQ page - app.get('/faq', async (req, res, next)=>{ - req.brew = { - title : 'FAQ', - text : faqText, - renderer : 'V3', - theme : '5ePHB' - }, - - req.ogMeta = { ...defaultMetaTags, - title : 'FAQ', - description : 'Frequently Asked Questions' - }; - - splitTextStyleAndMetadata(req.brew); - return next(); - }); - - //Source page - app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{ - const { brew } = req; - - const replaceStrings = { '&': '&', '<': '<', '>': '>' }; - let text = brew.text; - for (const replaceStr in replaceStrings) { - text = text.replaceAll(replaceStr, replaceStrings[replaceStr]); - } - text = `
${text}
`; - res.status(200).send(text); - }); - - //Download brew source page - app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{ - const { brew } = req; - sanitizeBrew(brew, 'share'); - const prefix = 'HB - '; - - const encodeRFC3986ValueChars = (str)=>{ - return ( - encodeURIComponent(str) - .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;}) - ); - }; - - let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); - if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; - res.set({ - 'Cache-Control' : 'no-cache', - 'Content-Type' : 'text/plain', - 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt` - }); - res.status(200).send(brew.text); - }); - //Serve brew metadata app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{ const { brew } = req; @@ -280,78 +168,6 @@ export default async function createApp(vite) { //Serve brew styling app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);}); - //User Page - app.get('/user/:username', dbCheck, async (req, res, next)=>{ - const ownAccount = req.account && (req.account.username == req.params.username); - - req.ogMeta = { ...defaultMetaTags, - title : `${req.params.username}'s Collection`, - description : 'View my collection of homebrew on the Homebrewery.' - // type : could be 'profile'? - }; - - const fields = [ - 'googleId', - 'title', - 'pageCount', - 'description', - 'authors', - 'lang', - 'published', - 'views', - 'shareId', - 'editId', - 'createdAt', - 'updatedAt', - 'lastViewed', - 'thumbnail', - 'tags' - ]; - - let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields) - .catch((err)=>{ - console.log(err); - }); - - brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed" - - if(ownAccount && req?.account?.googleId){ - const auth = await GoogleActions.authCheck(req.account, res); - let googleBrews = await GoogleActions.listGoogleBrews(auth) - .catch((err)=>{ - console.error(err); - }); - - // If stub matches file from Google, use Google metadata over stub metadata - if(googleBrews && googleBrews.length > 0) { - for (const brew of brews.filter((brew)=>brew.googleId)) { - const match = googleBrews.findIndex((b)=>b.editId === brew.editId); - if(match !== -1) { - brew.googleId = googleBrews[match].googleId; - brew.pageCount = googleBrews[match].pageCount; - brew.renderer = googleBrews[match].renderer; - brew.version = googleBrews[match].version; - brew.webViewLink = googleBrews[match].webViewLink; - googleBrews.splice(match, 1); - } - } - - //Remaining unstubbed google brews display current user as author - googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] })); - brews = _.concat(brews, googleBrews); - } - } - - req.brews = _.map(brews, (brew)=>{ - // Clean up brew data - brew.title = brew.title?.trim(); - brew.description = brew.description?.trim(); - return sanitizeBrew(brew, ownAccount ? 'edit' : 'share'); - }); - - return next(); - }); - //Change author name on brews app.put('/api/user/rename', dbCheck, async (req, res)=>{ const { username, newUsername } = req.body; @@ -380,144 +196,6 @@ export default async function createApp(vite) { } }); - //Edit Page - app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{ - req.brew = req.brew.toObject ? req.brew.toObject() : req.brew; - - req.userThemes = await(getUsersBrewThemes(req.account?.username)); - - req.ogMeta = { ...defaultMetaTags, - title : req.brew.title || 'Untitled Brew', - description : req.brew.description || 'No description.', - image : req.brew.thumbnail || defaultMetaTags.image, - locale : req.brew.lang, - type : 'article' - }; - - sanitizeBrew(req.brew, 'edit'); - splitTextStyleAndMetadata(req.brew); - res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save. - return next(); - })); - - //New Page from ID - app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{ - sanitizeBrew(req.brew, 'share'); - splitTextStyleAndMetadata(req.brew); - const brew = { - shareId : req.brew.shareId, - title : `CLONE - ${req.brew.title}`, - text : req.brew.text, - style : req.brew.style, - renderer : req.brew.renderer, - theme : req.brew.theme, - tags : req.brew.tags, - snippets : req.brew.snippets - }; - req.brew = _.defaults(brew, DEFAULT_BREW); - - req.userThemes = await(getUsersBrewThemes(req.account?.username)); - - req.ogMeta = { ...defaultMetaTags, - title : 'New', - description : 'Start crafting your homebrew on the Homebrewery!' - }; - - return next(); - })); - - //New Page - app.get('/new', asyncHandler(async(req, res, next)=>{ - req.userThemes = await(getUsersBrewThemes(req.account?.username)); - - req.ogMeta = { ...defaultMetaTags, - title : 'New', - description : 'Start crafting your homebrew on the Homebrewery!' - }; - - return next(); - })); - - //Share Page - app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ - const { brew } = req; - req.ogMeta = { ...defaultMetaTags, - title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, - description : req.brew.description || 'No description.', - image : req.brew.thumbnail || defaultMetaTags.image, - type : 'article' - }; - - // increase visitor view count, do not include visits by author(s) - if(!brew.authors.includes(req.account?.username)){ - if(req.params.id.length > 12 && !brew._id) { - const googleId = brew.googleId; - const shareId = brew.shareId; - await GoogleActions.increaseView(googleId, shareId, 'share', brew) - .catch((err)=>{next(err);}); - } else { - await HomebrewModel.increaseView({ shareId: brew.shareId }); - } - }; - - brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share'); - splitTextStyleAndMetadata(req.brew); - return next(); - })); - - //Account Page - app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ - const data = {}; - data.title = 'Account Information Page'; - - if(!req.account) { - res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"'); - const error = new Error('No valid account'); - error.status = 401; - error.HBErrorCode = '50'; - error.page = data.title; - return next(error); - }; - - let auth; - let googleCount = []; - if(req.account) { - if(req.account.googleId) { - auth = await GoogleActions.authCheck(req.account, res, false); - - googleCount = await GoogleActions.listGoogleBrews(auth) - .catch((err)=>{ - console.error(err); - }); - } - - const query = { authors: req.account.username, googleId: { $exists: false } }; - const mongoCount = await HomebrewModel.countDocuments(query) - .catch((err)=>{ - console.log(err); - return 0; - }); - - data.accountDetails = { - username : req.account.username, - issued : req.account.issued, - googleId : Boolean(req.account.googleId), - authCheck : Boolean(req.account.googleId && auth?.credentials.access_token), - mongoCount : mongoCount, - googleCount : googleCount?.length - }; - } - - req.brew = data; - - req.ogMeta = { ...defaultMetaTags, - title : `Account Page`, - description : null - }; - - return next(); - })); - // Local only if(isLocalEnvironment){ // Login @@ -534,15 +212,6 @@ export default async function createApp(vite) { app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages')); app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts')); - //Vault Page - app.get('/vault', asyncHandler(async(req, res, next)=>{ - req.ogMeta = { ...defaultMetaTags, - title : 'The Vault', - description : 'Search for Brews' - }; - return next(); - })); - //Send rendered page app.use(asyncHandler(async (req, res, next)=>{ if(!req.route) return res.redirect('/'); // Catch-all for invalid routes diff --git a/server/page-routes.js b/server/page-routes.js new file mode 100644 index 000000000..df9fc2c44 --- /dev/null +++ b/server/page-routes.js @@ -0,0 +1,377 @@ +/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ +// page-routes.js + +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +const __dirname = dirname(fileURLToPath(import.meta.url)); +process.chdir(`${__dirname}/..`); + +import _ from 'lodash'; +import express from 'express'; +import asyncHandler from 'express-async-handler'; + +//==== Middleware Imports ====// +import dbCheck from './middleware/dbCheck.js'; +import sanitizeFilename from 'sanitize-filename'; +import { DEFAULT_BREW } from './brewDefaults.js'; +import { splitTextStyleAndMetadata } from '../shared/helpers.js'; + +export default function pageRoutes({ + defaultMetaTags, + welcomeText, + welcomeTextLegacy, + migrateText, + changelogText, + faqText, + getBrew, + getUsersBrewThemes, + HomebrewModel, + GoogleActions, + sanitizeBrew, +}) { + const app = express.Router(); + + //Home page + app.get('/', (req, res, next)=>{ + req.brew = { + text : welcomeText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'Homepage', + description : 'Homepage' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Home page Legacy + app.get('/legacy', (req, res, next)=>{ + req.brew = { + text : welcomeTextLegacy, + renderer : 'legacy', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'Homepage (Legacy)', + description : 'Homepage' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Legacy/Other Document -> v3 Migration Guide + app.get('/migrate', (req, res, next)=>{ + req.brew = { + text : migrateText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'v3 Migration Guide', + description : 'A brief guide to converting Legacy documents to the v3 renderer.' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Changelog page + app.get('/changelog', async (req, res, next)=>{ + req.brew = { + title : 'Changelog', + text : changelogText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'Changelog', + description : 'Development changelog.' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //FAQ page + app.get('/faq', async (req, res, next)=>{ + req.brew = { + title : 'FAQ', + text : faqText, + renderer : 'V3', + theme : '5ePHB' + }, + + req.ogMeta = { ...defaultMetaTags, + title : 'FAQ', + description : 'Frequently Asked Questions' + }; + + splitTextStyleAndMetadata(req.brew); + return next(); + }); + + //Source page + app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{ + const { brew } = req; + + const replaceStrings = { '&': '&', '<': '<', '>': '>' }; + let text = brew.text; + for (const replaceStr in replaceStrings) { + text = text.replaceAll(replaceStr, replaceStrings[replaceStr]); + } + text = `
${text}
`; + res.status(200).send(text); + }); + + //Download brew source page + app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{ + const { brew } = req; + sanitizeBrew(brew, 'share'); + const prefix = 'HB - '; + + const encodeRFC3986ValueChars = (str)=>{ + return ( + encodeURIComponent(str) + .replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;}) + ); + }; + + let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', ''); + if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; }; + res.set({ + 'Cache-Control' : 'no-cache', + 'Content-Type' : 'text/plain', + 'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt` + }); + res.status(200).send(brew.text); + }); + + //User Page + app.get('/user/:username', dbCheck, async (req, res, next)=>{ + const ownAccount = req.account && (req.account.username == req.params.username); + + req.ogMeta = { ...defaultMetaTags, + title : `${req.params.username}'s Collection`, + description : 'View my collection of homebrew on the Homebrewery.' + // type : could be 'profile'? + }; + + const fields = [ + 'googleId', + 'title', + 'pageCount', + 'description', + 'authors', + 'lang', + 'published', + 'views', + 'shareId', + 'editId', + 'createdAt', + 'updatedAt', + 'lastViewed', + 'thumbnail', + 'tags' + ]; + + let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields) + .catch((err)=>{ + console.log(err); + }); + + brews.forEach((brew)=>brew.stubbed = true); //All brews from MongoDB are "stubbed" + + if(ownAccount && req?.account?.googleId){ + const auth = await GoogleActions.authCheck(req.account, res); + let googleBrews = await GoogleActions.listGoogleBrews(auth) + .catch((err)=>{ + console.error(err); + }); + + // If stub matches file from Google, use Google metadata over stub metadata + if(googleBrews && googleBrews.length > 0) { + for (const brew of brews.filter((brew)=>brew.googleId)) { + const match = googleBrews.findIndex((b)=>b.editId === brew.editId); + if(match !== -1) { + brew.googleId = googleBrews[match].googleId; + brew.pageCount = googleBrews[match].pageCount; + brew.renderer = googleBrews[match].renderer; + brew.version = googleBrews[match].version; + brew.webViewLink = googleBrews[match].webViewLink; + googleBrews.splice(match, 1); + } + } + + //Remaining unstubbed google brews display current user as author + googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] })); + brews = _.concat(brews, googleBrews); + } + } + + req.brews = _.map(brews, (brew)=>{ + // Clean up brew data + brew.title = brew.title?.trim(); + brew.description = brew.description?.trim(); + return sanitizeBrew(brew, ownAccount ? 'edit' : 'share'); + }); + + return next(); + }); + + //Edit Page + app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{ + req.brew = req.brew.toObject ? req.brew.toObject() : req.brew; + + req.userThemes = await(getUsersBrewThemes(req.account?.username)); + + req.ogMeta = { ...defaultMetaTags, + title : req.brew.title || 'Untitled Brew', + description : req.brew.description || 'No description.', + image : req.brew.thumbnail || defaultMetaTags.image, + locale : req.brew.lang, + type : 'article' + }; + + sanitizeBrew(req.brew, 'edit'); + splitTextStyleAndMetadata(req.brew); + res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save. + return next(); + })); + + //New Page from ID + app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{ + sanitizeBrew(req.brew, 'share'); + splitTextStyleAndMetadata(req.brew); + const brew = { + shareId : req.brew.shareId, + title : `CLONE - ${req.brew.title}`, + text : req.brew.text, + style : req.brew.style, + renderer : req.brew.renderer, + theme : req.brew.theme, + tags : req.brew.tags, + snippets : req.brew.snippets + }; + req.brew = _.defaults(brew, DEFAULT_BREW); + + req.userThemes = await(getUsersBrewThemes(req.account?.username)); + + req.ogMeta = { ...defaultMetaTags, + title : 'New', + description : 'Start crafting your homebrew on the Homebrewery!' + }; + + return next(); + })); + + //New Page + app.get('/new', asyncHandler(async(req, res, next)=>{ + req.userThemes = await(getUsersBrewThemes(req.account?.username)); + + req.ogMeta = { ...defaultMetaTags, + title : 'New', + description : 'Start crafting your homebrew on the Homebrewery!' + }; + + return next(); + })); + + //Share Page + app.get('/share/:id', dbCheck, asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{ + const { brew } = req; + req.ogMeta = { ...defaultMetaTags, + title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`, + description : req.brew.description || 'No description.', + image : req.brew.thumbnail || defaultMetaTags.image, + type : 'article' + }; + + // increase visitor view count, do not include visits by author(s) + if(!brew.authors.includes(req.account?.username)){ + if(req.params.id.length > 12 && !brew._id) { + const googleId = brew.googleId; + const shareId = brew.shareId; + await GoogleActions.increaseView(googleId, shareId, 'share', brew) + .catch((err)=>{next(err);}); + } else { + await HomebrewModel.increaseView({ shareId: brew.shareId }); + } + }; + + brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share'); + splitTextStyleAndMetadata(req.brew); + return next(); + })); + + //Account Page + app.get('/account', dbCheck, asyncHandler(async (req, res, next)=>{ + const data = {}; + data.title = 'Account Information Page'; + + if(!req.account) { + res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"'); + const error = new Error('No valid account'); + error.status = 401; + error.HBErrorCode = '50'; + error.page = data.title; + return next(error); + }; + + let auth; + let googleCount = []; + if(req.account) { + if(req.account.googleId) { + auth = await GoogleActions.authCheck(req.account, res, false); + + googleCount = await GoogleActions.listGoogleBrews(auth) + .catch((err)=>{ + console.error(err); + }); + } + + const query = { authors: req.account.username, googleId: { $exists: false } }; + const mongoCount = await HomebrewModel.countDocuments(query) + .catch((err)=>{ + console.log(err); + return 0; + }); + + data.accountDetails = { + username : req.account.username, + issued : req.account.issued, + googleId : Boolean(req.account.googleId), + authCheck : Boolean(req.account.googleId && auth?.credentials.access_token), + mongoCount : mongoCount, + googleCount : googleCount?.length + }; + } + + req.brew = data; + + req.ogMeta = { ...defaultMetaTags, + title : `Account Page`, + description : null + }; + + return next(); + })); + + //Vault Page + app.get('/vault', asyncHandler(async(req, res, next)=>{ + req.ogMeta = { ...defaultMetaTags, + title : 'The Vault', + description : 'Search for Brews' + }; + return next(); + })); + + return app; +} \ No newline at end of file From 18c29daf92fbd436183ba894e2ffd057f63ef666 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Sep 2026 13:20:45 -0400 Subject: [PATCH 07/27] Move resetWarnUnsavedTimer and ToggleAutosave to common hook --- client/homebrew/pages/editPage/editPage.jsx | 32 ++++++------------- client/homebrew/pages/homePage/homePage.jsx | 10 +----- client/homebrew/pages/newPage/newPage.jsx | 11 +------ .../homebrew/utils/commonEditPageFunctions.js | 25 +++++++++++++-- 4 files changed, 34 insertions(+), 44 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index dea5606f2..b9dbe9dce 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -39,8 +39,6 @@ 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_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; @@ -48,7 +46,7 @@ const SNIPKEY = 'HB_newPage_snippets'; const METAKEY = 'HB_newPage_meta'; const useLocalStorage = false; -const sandbox = false; +const sandbox = false; const EditPage = (props)=>{ props = { @@ -74,10 +72,9 @@ const EditPage = (props)=>{ const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); - const editorRef = useRef(null); - const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const saveTimeout = useRef(null); - const warnUnsavedTimeout = useRef(null); + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); + const saveTimeout = useRef(null); const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ ...prevBrew, @@ -86,12 +83,6 @@ const EditPage = (props)=>{ snippets : newData.snippets })); - 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 = ()=>{ if(currentBrew.authors.length > 0 && global.account?.username !== currentBrew.authors[0]) { setAlertOwnershipToTransfer(true); @@ -272,14 +263,6 @@ const EditPage = (props)=>{ return saved; }; - const toggleAutoSave = ()=>{ - clearTimeout(warnUnsavedTimeout.current); - clearTimeout(saveTimeout.current); - localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled)); - setAutoSaveEnabled(!autoSaveEnabled); - setWarnUnsavedChanges(autoSaveEnabled); - }; - const renderAutoSaveButton = ()=>( Autosave @@ -317,8 +300,10 @@ const EditPage = (props)=>{ }; const { + resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + toggleAutoSave } = useCommonEditPageFunctions({ saveGoogle, setError, @@ -341,7 +326,8 @@ const EditPage = (props)=>{ trySave, sandbox, lastSavedBrew, - editorRef + editorRef, + saveTimeout }); return ( diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 1e02d67b8..7685f7bc5 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -32,8 +32,6 @@ import Headtags from '@vitreum/headtags.js'; const Meta = Headtags.Meta; 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 BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; @@ -64,7 +62,6 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const warnUnsavedTimeout = useRef(null); const save = ()=>{ request.post('/api') @@ -79,12 +76,6 @@ const HomePage =(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 - }; - const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING if(isSaving) @@ -142,6 +133,7 @@ const HomePage =(props)=>{ }; const { + resetWarnUnsavedTimer, handleSplitMove, handleBrewChange } = useCommonEditPageFunctions({ diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 8182ac20c..9db9f11f2 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -29,8 +29,6 @@ const { both: RecentNavItem } = RecentNavItems; // Page specific imports 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 BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; @@ -64,8 +62,6 @@ const NewPage = (props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - // const saveTimeout = useRef(null); - const warnUnsavedTimeout = useRef(null); useEffect(()=>{ loadBrew(); @@ -100,12 +96,6 @@ const NewPage = (props)=>{ window.history.replaceState({}, window.location.title, '/new/'); }; - 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 trySave = useEffectEvent(async ()=>{ setIsSaving(true); @@ -196,6 +186,7 @@ const NewPage = (props)=>{ ); const { + resetWarnUnsavedTimer, handleSplitMove, handleBrewChange } = useCommonEditPageFunctions({ diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index ff53981f8..f0c398ae4 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -4,6 +4,9 @@ import _ from 'lodash'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; +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 + export default function useCommonEditPageFunctions(dependencies) { const { setError, @@ -27,10 +30,12 @@ export default function useCommonEditPageFunctions(dependencies) { unsavedChanges, setUnsavedChanges, lastSavedBrew, - editorRef + editorRef, + saveTimeout = undefined } = dependencies; const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges + const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time //==--------- Page setup ----------==// useEffect(()=>{ @@ -70,6 +75,12 @@ export default function useCommonEditPageFunctions(dependencies) { if(autoSaveEnabled) trySave(false, hasChange, saveGoogle); }, [currentBrew]); + 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 handleSplitMove = ()=>{ editorRef.current.update(); }; @@ -97,8 +108,18 @@ export default function useCommonEditPageFunctions(dependencies) { } }; + const toggleAutoSave = ()=>{ + clearTimeout(warnUnsavedTimeout.current); + clearTimeout(saveTimeout.current); + localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled)); + setAutoSaveEnabled(!autoSaveEnabled); + setWarnUnsavedChanges(autoSaveEnabled); + }; + return { + resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + toggleAutoSave } } \ No newline at end of file From 3e2ac93424d248461cb82eef465c3db7815c6849 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Sun, 13 Sep 2026 16:24:45 -0400 Subject: [PATCH 08/27] Make trySave common and move to useCommonEditorFunctions Also makes "save()" more common between the pages, but they still have some differences --- client/homebrew/pages/editPage/editPage.jsx | 32 ++++----------- client/homebrew/pages/homePage/homePage.jsx | 40 +++++++++++-------- client/homebrew/pages/newPage/newPage.jsx | 40 ++++++++++--------- .../homebrew/utils/commonEditPageFunctions.js | 35 +++++++++++++--- 4 files changed, 82 insertions(+), 65 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index b9dbe9dce..a0287236b 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -38,8 +38,6 @@ import LockNotification from './lockNotification/lockNotification.jsx'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; import googleDriveIcon from '../../googleDrive.svg'; -const SAVE_TIMEOUT = 10000; - const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -74,7 +72,6 @@ const EditPage = (props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const saveTimeout = useRef(null); const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ ...prevBrew, @@ -113,25 +110,6 @@ const EditPage = (props)=>{ trySave(true, true, newSaveGoogle); }; - const trySave = useEffectEvent((immediate = false, hasChanges = true, saveToGoogle = false)=>{ - clearTimeout(saveTimeout.current); - if(isSaving) return; - if(!hasChanges && !immediate) return; - const newTimeout = immediate ? 0 : SAVE_TIMEOUT; - - saveTimeout.current = setTimeout(async ()=>{ - setIsSaving(true); - setError(null); - await save(currentBrew, saveToGoogle) - .catch((err)=>{ - setError(err); - }); - setIsSaving(false); - setLastSavedTime(new Date()); - if(!autoSaveEnabled) resetWarnUnsavedTimer(); - }, newTimeout); - }); - const save = async (brew, saveToGoogle)=>{ setHTMLErrors(hbfm.validate(brew.text)); @@ -303,7 +281,8 @@ const EditPage = (props)=>{ resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, - toggleAutoSave + toggleAutoSave, + trySave } = useCommonEditPageFunctions({ saveGoogle, setError, @@ -323,11 +302,14 @@ const EditPage = (props)=>{ setWarnUnsavedChanges, unsavedChanges, setUnsavedChanges, - trySave, sandbox, lastSavedBrew, editorRef, - saveTimeout + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime }); return ( diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 7685f7bc5..a1a4a257e 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -31,8 +31,6 @@ const { both: RecentNavItem } = RecentNavItems; import Headtags from '@vitreum/headtags.js'; const Meta = Headtags.Meta; -const SAVE_TIMEOUT = 10000; - const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -48,6 +46,7 @@ const HomePage =(props)=>{ }; const [currentBrew, setCurrentBrew] = useState(props.brew); + const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false); const [error, setError] = useState(undefined); const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text)); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1); @@ -63,17 +62,19 @@ const HomePage =(props)=>{ const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); - const save = ()=>{ - request.post('/api') - .send(currentBrew) - .end((err, res)=>{ - if(err) { - setError(err); - return; - } - const saved = res.body; - window.location = `/edit/${saved.editId}`; + const save = async (brew, saveToGoogle)=>{ + const res = await request + .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) + .send(brew) + .catch((err)=>{ + console.error('Error Updating Local Brew'); + setError(err); }); + if(!res) return; + + const saved = res.body; + window.onbeforeunload = null; + window.location = `/edit/${saved.editId}`; }; const renderSaveButton = ()=>{ @@ -97,7 +98,7 @@ const HomePage =(props)=>{ // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) - return save now; + return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED if(autoSaveEnabled) @@ -135,8 +136,10 @@ const HomePage =(props)=>{ const { resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + trySave } = useCommonEditPageFunctions({ + saveGoogle, setError, setThemeBundle, HTMLErrors, @@ -156,7 +159,12 @@ const HomePage =(props)=>{ setUnsavedChanges, sandbox, lastSavedBrew, - editorRef + editorRef, + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime }); return ( @@ -190,7 +198,7 @@ const HomePage =(props)=>{ /> -
+
trySave(true, true, saveGoogle)}> Save current
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 9db9f11f2..1ec1720f4 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -28,8 +28,6 @@ import RecentNavItems from '@navbar/recent.navitem.jsx'; const { both: RecentNavItem } = RecentNavItems; // Page specific imports -const SAVE_TIMEOUT = 10000; - const BREWKEY = 'HB_newPage_content'; const STYLEKEY = 'HB_newPage_style'; const SNIPKEY = 'HB_newPage_snippets'; @@ -96,24 +94,22 @@ const NewPage = (props)=>{ window.history.replaceState({}, window.location.title, '/new/'); }; - const trySave = useEffectEvent(async ()=>{ - setIsSaving(true); - - const updatedBrew = { ...currentBrew }; - splitTextStyleAndMetadata(updatedBrew); - - const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/gm; - updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1; + const save = async (brew, saveToGoogle)=>{ + //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(?:break)?(?: *{[^\n{}]*})?$)/gm)) || []).length + 1, + textBin : undefined + }; const res = await request .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) - .send(updatedBrew) + .send(brewToSave) .catch((err)=>{ - setIsSaving(false); + console.error('Error Updating Local Brew'); setError(err); }); - - setIsSaving(false); if(!res) return; const savedBrew = res.body; @@ -123,7 +119,7 @@ const NewPage = (props)=>{ localStorage.removeItem(METAKEY); window.onbeforeunload = null; window.location = `/edit/${savedBrew.editId}`; - }); + }; const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING @@ -146,7 +142,7 @@ const NewPage = (props)=>{ // #3 - Unsaved changes exist, click to save, show SAVE NOW if(unsavedChanges) - return save now; + return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED if(autoSaveEnabled) @@ -188,8 +184,10 @@ const NewPage = (props)=>{ const { resetWarnUnsavedTimer, handleSplitMove, - handleBrewChange + handleBrewChange, + trySave } = useCommonEditPageFunctions({ + saveGoogle, setError, setThemeBundle, HTMLErrors, @@ -207,10 +205,14 @@ const NewPage = (props)=>{ setWarnUnsavedChanges, unsavedChanges, setUnsavedChanges, - trySave, sandbox, lastSavedBrew, - editorRef + editorRef, + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime }); return ( diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index f0c398ae4..e49e29136 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -1,14 +1,16 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useEffectEvent, useRef } from 'react'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import _ from 'lodash'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; +const SAVE_TIMEOUT = 10000; //Autosave 10 seconds after last change 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 export default function useCommonEditPageFunctions(dependencies) { const { + saveGoogle, setError, setThemeBundle, HTMLErrors, @@ -24,18 +26,21 @@ export default function useCommonEditPageFunctions(dependencies) { autoSaveEnabled, setAutoSaveEnabled, setWarnUnsavedChanges, - trySave = ()=>{}, sandbox, - saveGoogle = false, unsavedChanges, setUnsavedChanges, lastSavedBrew, editorRef, - saveTimeout = undefined + isSaving, + setIsSaving, + save, + lastSavedTime, + setLastSavedTime } = dependencies; const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time + const saveTimeout = useRef(null); //==--------- Page setup ----------==// useEffect(()=>{ @@ -116,10 +121,30 @@ export default function useCommonEditPageFunctions(dependencies) { setWarnUnsavedChanges(autoSaveEnabled); }; + const trySave = useEffectEvent((forceSave = false, hasChanges = true, saveToGoogle = false)=>{ + clearTimeout(saveTimeout.current); + if(isSaving) return; + if(!forceSave && !hasChanges) return; + const newTimeout = forceSave ? 0 : SAVE_TIMEOUT; + + saveTimeout.current = setTimeout(async ()=>{ + setIsSaving(true); + setError(null); + await save(currentBrew, saveToGoogle) + .catch((err)=>{ + setError(err); + }); + setIsSaving(false); + setLastSavedTime(new Date()); + if(!autoSaveEnabled) resetWarnUnsavedTimer(); + }, newTimeout); + }); + return { resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, - toggleAutoSave + toggleAutoSave, + trySave, } } \ No newline at end of file From 321b8cfc1b803b7c56669aacefc42a5359054a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 23:12:56 +0200 Subject: [PATCH 09/27] simplify imports as suggested --- server/app.js | 24 +++++------------------- server/page-routes.js | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/server/app.js b/server/app.js index 9e04c7b5b..eb078a877 100644 --- a/server/app.js +++ b/server/app.js @@ -16,11 +16,11 @@ import path from 'path'; import fs from 'fs-extra'; import api from './homebrew.api.js'; -import pageRoutes from './page-routes.js'; -const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api; +const { homebrewApi, getBrew, getCSS } = api; import adminApi from './admin.api.js'; import vaultApi from './vault.api.js'; -import GoogleActions from './googleActions.js'; +import pageRoutes from './page-routes.js'; + import serveCompressedStaticAssets from './static-assets.mv.js'; import asyncHandler from 'express-async-handler'; import { model as HomebrewModel } from './homebrew.model.js'; @@ -113,12 +113,6 @@ export default async function createApp(vite) { app.use(adminApi(vite)); app.use(vaultApi); - const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); - const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8'); - const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8'); - const changelogText = fs.readFileSync('changelog.md', 'utf8'); - const faqText = fs.readFileSync('faq.md', 'utf8'); - String.prototype.replaceAll = function(s, r){return this.split(s).join(r);}; const defaultMetaTags = { @@ -131,16 +125,8 @@ export default async function createApp(vite) { app.use(pageRoutes({ defaultMetaTags, - welcomeText, - welcomeTextLegacy, - migrateText, - changelogText, - faqText, - getBrew, - getUsersBrewThemes, - HomebrewModel, - GoogleActions, - sanitizeBrew, + HomebrewModel, + sanitizeBrew, })); //Robots.txt diff --git a/server/page-routes.js b/server/page-routes.js index df9fc2c44..c357db7dc 100644 --- a/server/page-routes.js +++ b/server/page-routes.js @@ -15,18 +15,20 @@ import dbCheck from './middleware/dbCheck.js'; import sanitizeFilename from 'sanitize-filename'; import { DEFAULT_BREW } from './brewDefaults.js'; import { splitTextStyleAndMetadata } from '../shared/helpers.js'; +import GoogleActions from './googleActions.js'; + +import api from './homebrew.api.js'; +const { getBrew, getUsersBrewThemes } = api; + +const welcomeText = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); +const welcomeTextLegacy = fs.readFileSync('./client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8'); +const migrateText = fs.readFileSync('./client/homebrew/pages/homePage/migrate.md', 'utf8'); +const changelogText = fs.readFileSync('changelog.md', 'utf8'); +const faqText = fs.readFileSync('faq.md', 'utf8'); export default function pageRoutes({ defaultMetaTags, - welcomeText, - welcomeTextLegacy, - migrateText, - changelogText, - faqText, - getBrew, - getUsersBrewThemes, HomebrewModel, - GoogleActions, sanitizeBrew, }) { const app = express.Router(); From 402dfebcf0ccf6533ae7e0b59445b65245b89a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Sun, 13 Sep 2026 23:18:10 +0200 Subject: [PATCH 10/27] add fs --- server/page-routes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/page-routes.js b/server/page-routes.js index c357db7dc..e2e7be193 100644 --- a/server/page-routes.js +++ b/server/page-routes.js @@ -9,13 +9,14 @@ process.chdir(`${__dirname}/..`); import _ from 'lodash'; import express from 'express'; import asyncHandler from 'express-async-handler'; +import fs from 'fs'; //==== Middleware Imports ====// import dbCheck from './middleware/dbCheck.js'; import sanitizeFilename from 'sanitize-filename'; import { DEFAULT_BREW } from './brewDefaults.js'; import { splitTextStyleAndMetadata } from '../shared/helpers.js'; -import GoogleActions from './googleActions.js'; +import GoogleActions from './googleActions.js'; import api from './homebrew.api.js'; const { getBrew, getUsersBrewThemes } = api; From c14d1833b15a1ebd6764ac2450f1511018201158 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 14 Sep 2026 20:33:54 +1000 Subject: [PATCH 11/27] add named aliases for first-letter drop-cap style --- themes/Legacy/5ePHB/style.less | 15 ++++++++------- themes/V3/5eDMG/style.less | 1 + themes/V3/5ePHB/snippets.js | 1 + themes/V3/5ePHB/style.less | 35 +++++++++++++++++----------------- themes/V3/Journal/style.less | 27 +++++++++++++------------- 5 files changed, 42 insertions(+), 37 deletions(-) diff --git a/themes/Legacy/5ePHB/style.less b/themes/Legacy/5ePHB/style.less index 41524c74c..e09e53682 100644 --- a/themes/Legacy/5ePHB/style.less +++ b/themes/Legacy/5ePHB/style.less @@ -115,13 +115,14 @@ body { counter-reset : phb-page-numbers; } font-size : 0.987cm; -webkit-column-span : all; -moz-column-span : all; - & + p::first-letter { - float : left; - font-family : 'Solberry'; - font-size : 10em; - line-height : 0.795em; - color : #222222; - } + } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + font-family : 'Solberry'; + font-size : 10em; + line-height : 0.795em; + color : #222222; } h2 { font-size : 0.705cm; } h3 { diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less index d79533c2c..8aad90315 100644 --- a/themes/V3/5eDMG/style.less +++ b/themes/V3/5eDMG/style.less @@ -14,6 +14,7 @@ .note table tbody tr:nth-child(odd) { background : #FFFFFF; } /* DROP CAP */ + .first-letter, .drop-cap h1 + p::first-letter { color : black; background-image : unset; diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index fee12e1f6..73ac5cb43 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -30,6 +30,7 @@ export default [ name : 'Tweak Drop Cap', icon : 'fas fa-sliders-h', gen : dedent`/* Drop Cap settings */ + .page .first-letter, .page .drop-cap, .page h1 + p::first-letter { font-family: SolberaImitationRemake; font-size: 3.5cm; diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 12601bb5d..28bde8a2f 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -85,23 +85,24 @@ line-height : 1em; -webkit-column-span : all; -moz-column-span : all; - & + p::first-letter { - float : left; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'SolberaImitationRemake'; - font-size : 3.5cm; - line-height : 1em; - color : rgba(0, 0, 0, 0); - background-image : linear-gradient(-45deg, #322814, #998250, #322814); - -webkit-background-clip : text; - background-clip : text; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'SolberaImitationRemake'; + font-size : 3.5cm; + line-height : 1em; + color : rgba(0, 0, 0, 0); + background-image : linear-gradient(-45deg, #322814, #998250, #322814); + -webkit-background-clip : text; + background-clip : text; + } h2 { //margin-top : 0px; //Font is misaligned. Shift up slightly //margin-bottom : 0.05cm; @@ -196,7 +197,7 @@ } & + * { margin-top : 0.54cm; } - + } // ***************************** @@ -279,7 +280,7 @@ .watermark { color : black; } /* Watercolor */ - + .watercolor1 { --wc : @watercolor1; } .watercolor2 { --wc : @watercolor2; } .watercolor3 { --wc : @watercolor3; } diff --git a/themes/V3/Journal/style.less b/themes/V3/Journal/style.less index 74c976f47..9b9b7d024 100644 --- a/themes/V3/Journal/style.less +++ b/themes/V3/Journal/style.less @@ -80,21 +80,22 @@ font-size : 0.89cm; font-variant : small-caps; line-height : 1em; - & + p::first-letter { - float : left; - padding-top : 0.3em; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-right : 0.1em; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'FrederickaTheGreat'; - font-size : 1.9em; - line-height : 1em; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-top : 0.3em; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-right : 0.1em; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'FrederickaTheGreat'; + font-size : 1.9em; + line-height : 1em; + } h2 { font-size : 0.62cm; line-height : 0.988em; //Font is misaligned. Shift up slightly From a9be3d913dc6ac147ffb2b14b187cca845961ec9 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 14 Sep 2026 22:29:03 +1000 Subject: [PATCH 12/27] Revert "add named aliases for first-letter drop-cap style" This reverts commit c14d1833b15a1ebd6764ac2450f1511018201158. --- themes/V3/5eDMG/style.less | 1 - themes/V3/5ePHB/snippets.js | 1 - themes/V3/5ePHB/style.less | 31 +++++++++++++++---------------- themes/V3/Journal/style.less | 27 +++++++++++++-------------- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less index 8aad90315..d79533c2c 100644 --- a/themes/V3/5eDMG/style.less +++ b/themes/V3/5eDMG/style.less @@ -14,7 +14,6 @@ .note table tbody tr:nth-child(odd) { background : #FFFFFF; } /* DROP CAP */ - .first-letter, .drop-cap h1 + p::first-letter { color : black; background-image : unset; diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index 73ac5cb43..fee12e1f6 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -30,7 +30,6 @@ export default [ name : 'Tweak Drop Cap', icon : 'fas fa-sliders-h', gen : dedent`/* Drop Cap settings */ - .page .first-letter, .page .drop-cap, .page h1 + p::first-letter { font-family: SolberaImitationRemake; font-size: 3.5cm; diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 28bde8a2f..694c33d15 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -85,24 +85,23 @@ line-height : 1em; -webkit-column-span : all; -moz-column-span : all; + & + p::first-letter { + float : left; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'SolberaImitationRemake'; + font-size : 3.5cm; + line-height : 1em; + color : rgba(0, 0, 0, 0); + background-image : linear-gradient(-45deg, #322814, #998250, #322814); + -webkit-background-clip : text; + background-clip : text; + } & + p::first-line { font-variant : small-caps; } } - .first-letter, .drop-cap, - h1 + p::first-letter { - float : left; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'SolberaImitationRemake'; - font-size : 3.5cm; - line-height : 1em; - color : rgba(0, 0, 0, 0); - background-image : linear-gradient(-45deg, #322814, #998250, #322814); - -webkit-background-clip : text; - background-clip : text; - } h2 { //margin-top : 0px; //Font is misaligned. Shift up slightly //margin-bottom : 0.05cm; diff --git a/themes/V3/Journal/style.less b/themes/V3/Journal/style.less index 9b9b7d024..74c976f47 100644 --- a/themes/V3/Journal/style.less +++ b/themes/V3/Journal/style.less @@ -80,22 +80,21 @@ font-size : 0.89cm; font-variant : small-caps; line-height : 1em; + & + p::first-letter { + float : left; + padding-top : 0.3em; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-right : 0.1em; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'FrederickaTheGreat'; + font-size : 1.9em; + line-height : 1em; + } & + p::first-line { font-variant : small-caps; } } - .first-letter, .drop-cap, - h1 + p::first-letter { - float : left; - padding-top : 0.3em; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-right : 0.1em; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'FrederickaTheGreat'; - font-size : 1.9em; - line-height : 1em; - } h2 { font-size : 0.62cm; line-height : 0.988em; //Font is misaligned. Shift up slightly From dafec31632065a735db323c3ab3405a80f88cf41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Mon, 14 Sep 2026 16:10:47 +0200 Subject: [PATCH 13/27] basic fix --- server/homebrew.api.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 4cca76193..3e3554b66 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -168,8 +168,7 @@ const api = { const googleBrew = await GoogleActions.getGoogleBrew(oAuth2Client, googleId, id, accessType) .catch((googleError)=>{ - const reason = googleError.errors?.[0].reason; - if(reason == 'notFound') + if(googleError.code === 404 || googleError.status === 404) throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username }; else throw { ...googleError, HBErrorCode: '01' }; From 79df1dc9162314a57617f203239fe9913b9e35e4 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 15 Sep 2026 00:43:55 +1000 Subject: [PATCH 14/27] Reapply "add named aliases for first-letter drop-cap style" This reverts commit a9be3d913dc6ac147ffb2b14b187cca845961ec9. --- themes/V3/5eDMG/style.less | 1 + themes/V3/5ePHB/snippets.js | 1 + themes/V3/5ePHB/style.less | 31 ++++++++++++++++--------------- themes/V3/Journal/style.less | 27 ++++++++++++++------------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/themes/V3/5eDMG/style.less b/themes/V3/5eDMG/style.less index d79533c2c..8aad90315 100644 --- a/themes/V3/5eDMG/style.less +++ b/themes/V3/5eDMG/style.less @@ -14,6 +14,7 @@ .note table tbody tr:nth-child(odd) { background : #FFFFFF; } /* DROP CAP */ + .first-letter, .drop-cap h1 + p::first-letter { color : black; background-image : unset; diff --git a/themes/V3/5ePHB/snippets.js b/themes/V3/5ePHB/snippets.js index fee12e1f6..73ac5cb43 100644 --- a/themes/V3/5ePHB/snippets.js +++ b/themes/V3/5ePHB/snippets.js @@ -30,6 +30,7 @@ export default [ name : 'Tweak Drop Cap', icon : 'fas fa-sliders-h', gen : dedent`/* Drop Cap settings */ + .page .first-letter, .page .drop-cap, .page h1 + p::first-letter { font-family: SolberaImitationRemake; font-size: 3.5cm; diff --git a/themes/V3/5ePHB/style.less b/themes/V3/5ePHB/style.less index 694c33d15..28bde8a2f 100644 --- a/themes/V3/5ePHB/style.less +++ b/themes/V3/5ePHB/style.less @@ -85,23 +85,24 @@ line-height : 1em; -webkit-column-span : all; -moz-column-span : all; - & + p::first-letter { - float : left; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'SolberaImitationRemake'; - font-size : 3.5cm; - line-height : 1em; - color : rgba(0, 0, 0, 0); - background-image : linear-gradient(-45deg, #322814, #998250, #322814); - -webkit-background-clip : text; - background-clip : text; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'SolberaImitationRemake'; + font-size : 3.5cm; + line-height : 1em; + color : rgba(0, 0, 0, 0); + background-image : linear-gradient(-45deg, #322814, #998250, #322814); + -webkit-background-clip : text; + background-clip : text; + } h2 { //margin-top : 0px; //Font is misaligned. Shift up slightly //margin-bottom : 0.05cm; diff --git a/themes/V3/Journal/style.less b/themes/V3/Journal/style.less index 74c976f47..9b9b7d024 100644 --- a/themes/V3/Journal/style.less +++ b/themes/V3/Journal/style.less @@ -80,21 +80,22 @@ font-size : 0.89cm; font-variant : small-caps; line-height : 1em; - & + p::first-letter { - float : left; - padding-top : 0.3em; - padding-bottom : 2px; - padding-left : 40px; //Allow background color to extend into margins - margin-top : -0.3cm; - margin-right : 0.1em; - margin-bottom : -20px; - margin-left : -40px; - font-family : 'FrederickaTheGreat'; - font-size : 1.9em; - line-height : 1em; - } & + p::first-line { font-variant : small-caps; } } + .first-letter, .drop-cap, + h1 + p::first-letter { + float : left; + padding-top : 0.3em; + padding-bottom : 2px; + padding-left : 40px; //Allow background color to extend into margins + margin-top : -0.3cm; + margin-right : 0.1em; + margin-bottom : -20px; + margin-left : -40px; + font-family : 'FrederickaTheGreat'; + font-size : 1.9em; + line-height : 1em; + } h2 { font-size : 0.62cm; line-height : 0.988em; //Font is misaligned. Shift up slightly From e44706852cfedc3ac0684f7af600dafd3b361967 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 15 Sep 2026 00:55:14 +1000 Subject: [PATCH 15/27] revert legacy file changes --- themes/Legacy/5ePHB/style.less | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/themes/Legacy/5ePHB/style.less b/themes/Legacy/5ePHB/style.less index e09e53682..41524c74c 100644 --- a/themes/Legacy/5ePHB/style.less +++ b/themes/Legacy/5ePHB/style.less @@ -115,14 +115,13 @@ body { counter-reset : phb-page-numbers; } font-size : 0.987cm; -webkit-column-span : all; -moz-column-span : all; - } - .first-letter, .drop-cap, - h1 + p::first-letter { - float : left; - font-family : 'Solberry'; - font-size : 10em; - line-height : 0.795em; - color : #222222; + & + p::first-letter { + float : left; + font-family : 'Solberry'; + font-size : 10em; + line-height : 0.795em; + color : #222222; + } } h2 { font-size : 0.705cm; } h3 { From 13731f57710809ba097438a3c39efd4e9e9df272 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 14 Sep 2026 19:11:14 +0000 Subject: [PATCH 16/27] Move clearError to common hook --- client/homebrew/pages/editPage/editPage.jsx | 6 +----- client/homebrew/pages/homePage/homePage.jsx | 6 +----- client/homebrew/pages/newPage/newPage.jsx | 6 +----- client/homebrew/utils/commonEditPageFunctions.js | 6 ++++++ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index a0287236b..1c1cceb7a 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -247,11 +247,6 @@ const EditPage = (props)=>{ ); - const clearError = ()=>{ - setError(null); - setIsSaving(false); - }; - const renderNavbar = ()=>{ return @@ -282,6 +277,7 @@ const EditPage = (props)=>{ handleSplitMove, handleBrewChange, toggleAutoSave, + clearError, trySave } = useCommonEditPageFunctions({ saveGoogle, diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index a1a4a257e..b7e6bff82 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -112,11 +112,6 @@ const HomePage =(props)=>{ return saved; }; - const clearError = ()=>{ - setError(null); - setIsSaving(false); - }; - const renderNavbar = ()=>{ return @@ -137,6 +132,7 @@ const HomePage =(props)=>{ resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, + clearError, trySave } = useCommonEditPageFunctions({ saveGoogle, diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 1ec1720f4..2e756bc0d 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -156,11 +156,6 @@ const NewPage = (props)=>{ return saved; }; - const clearError = ()=>{ - setError(null); - setIsSaving(false); - }; - const renderNavbar = ()=>( @@ -185,6 +180,7 @@ const NewPage = (props)=>{ resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, + clearError, trySave } = useCommonEditPageFunctions({ saveGoogle, diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index e49e29136..7d23a1fb4 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -121,6 +121,11 @@ export default function useCommonEditPageFunctions(dependencies) { setWarnUnsavedChanges(autoSaveEnabled); }; + const clearError = ()=>{ + setError(null); + setIsSaving(false); + }; + const trySave = useEffectEvent((forceSave = false, hasChanges = true, saveToGoogle = false)=>{ clearTimeout(saveTimeout.current); if(isSaving) return; @@ -145,6 +150,7 @@ export default function useCommonEditPageFunctions(dependencies) { handleSplitMove, handleBrewChange, toggleAutoSave, + clearError, trySave, } } \ No newline at end of file From 7c2344460f5e71788084bd2b771dc22502c4e105 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 14 Sep 2026 21:08:04 +0000 Subject: [PATCH 17/27] Move RenderSaveButton over --- client/homebrew/pages/editPage/editPage.jsx | 52 +------------------ client/homebrew/pages/homePage/homePage.jsx | 52 +------------------ client/homebrew/pages/newPage/newPage.jsx | 52 +------------------ .../homebrew/utils/commonEditPageFunctions.js | 50 ++++++++++++++---- 4 files changed, 44 insertions(+), 162 deletions(-) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 1c1cceb7a..d8f550ff4 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -53,8 +53,6 @@ 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(hbfm.validate(props.brew.text)); @@ -62,13 +60,10 @@ const EditPage = (props)=>{ 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 [alertNoGoogleToTransfer, setAlertNoGoogleToTransfer] = useState(false); const [alertOwnershipToTransfer, setAlertOwnershipToTransfer] = useState(false); const [confirmGoogleTransfer, setConfirmGoogleTransfer] = useState(false); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(true); - const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -206,41 +201,6 @@ const EditPage = (props)=>{ ); - const renderSaveButton = ()=>{ - // #1 - Currently saving, show SAVING - if(isSaving) - return saving...; - - // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(unsavedChanges && warnUnsavedChanges) { - resetWarnUnsavedTimer(); - const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; - - return - Reminder... -
{text}
-
; - } - - // #3 - Unsaved changes exist, click to save, show SAVE NOW - if(unsavedChanges) - return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; - - // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(autoSaveEnabled) - return auto-saved; - - // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button - if(sandbox) - return save now; - - // DEFAULT - No unsaved changes, show SAVED - return saved; - }; - const renderAutoSaveButton = ()=>( Autosave @@ -273,11 +233,12 @@ const EditPage = (props)=>{ }; const { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, toggleAutoSave, clearError, + renderSaveButton, + autoSaveEnabled, trySave } = useCommonEditPageFunctions({ saveGoogle, @@ -293,19 +254,10 @@ const EditPage = (props)=>{ SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - unsavedChanges, - setUnsavedChanges, sandbox, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime }); return ( diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index b7e6bff82..d571c74ea 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -53,11 +53,6 @@ const HomePage =(props)=>{ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle, setThemeBundle] = useState({}); - const [unsavedChanges, setUnsavedChanges] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [lastSavedTime, setLastSavedTime] = useState(new Date()); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); - const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -77,41 +72,6 @@ const HomePage =(props)=>{ window.location = `/edit/${saved.editId}`; }; - const renderSaveButton = ()=>{ - // #1 - Currently saving, show SAVING - if(isSaving) - return saving...; - - // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(unsavedChanges && warnUnsavedChanges) { - resetWarnUnsavedTimer(); - const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; - - return - Reminder... -
{text}
-
; - } - - // #3 - Unsaved changes exist, click to save, show SAVE NOW - if(unsavedChanges) - return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; - - // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(autoSaveEnabled) - return auto-saved; - - // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button - if(sandbox) - return save now; - - // DEFAULT - No unsaved changes, show SAVED - return saved; - }; - const renderNavbar = ()=>{ return @@ -129,10 +89,11 @@ const HomePage =(props)=>{ }; const { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, clearError, + renderSaveButton, + unsavedChanges, trySave } = useCommonEditPageFunctions({ saveGoogle, @@ -148,19 +109,10 @@ const HomePage =(props)=>{ SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - unsavedChanges, - setUnsavedChanges, sandbox, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime }); return ( diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 2e756bc0d..bbeb1ed52 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -45,8 +45,6 @@ const NewPage = (props)=>{ }; const [currentBrew, setCurrentBrew] = useState(props.brew); - const [isSaving, setIsSaving] = useState(false); - const [lastSavedTime, setLastSavedTime] = useState(new Date()); const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false); const [error, setError] = useState(null); const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text)); @@ -54,9 +52,6 @@ const NewPage = (props)=>{ const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [themeBundle, setThemeBundle] = useState({}); - const [unsavedChanges, setUnsavedChanges] = useState(false); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(false); - const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); const editorRef = useRef(null); const lastSavedBrew = useRef(_.cloneDeep(props.brew)); @@ -121,41 +116,6 @@ const NewPage = (props)=>{ window.location = `/edit/${savedBrew.editId}`; }; - const renderSaveButton = ()=>{ - // #1 - Currently saving, show SAVING - if(isSaving) - return saving...; - - // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(unsavedChanges && warnUnsavedChanges) { - resetWarnUnsavedTimer(); - const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); - const text = elapsedTime === 0 - ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` - : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; - - return - Reminder... -
{text}
-
; - } - - // #3 - Unsaved changes exist, click to save, show SAVE NOW - if(unsavedChanges) - return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; - - // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(autoSaveEnabled) - return auto-saved; - - // #5 - Sandbox with no unsaved changes, and has never been saved, hide the button - if(sandbox) - return save now; - - // DEFAULT - No unsaved changes, show SAVED - return saved; - }; - const renderNavbar = ()=>( @@ -177,11 +137,10 @@ const NewPage = (props)=>{ ); const { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, clearError, - trySave + renderSaveButton } = useCommonEditPageFunctions({ saveGoogle, setError, @@ -196,19 +155,10 @@ const NewPage = (props)=>{ SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, - unsavedChanges, - setUnsavedChanges, sandbox, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime }); return ( diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.js index 7d23a1fb4..89cd2e35e 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useEffectEvent, useRef } from 'react'; import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js'; import _ from 'lodash'; +import Nav from '@navbar/nav.jsx'; const AUTOSAVE_KEY = 'HB_editor_autoSaveOn'; @@ -23,21 +24,18 @@ export default function useCommonEditPageFunctions(dependencies) { SNIPKEY, METAKEY, hbfm, - autoSaveEnabled, - setAutoSaveEnabled, - setWarnUnsavedChanges, sandbox, - unsavedChanges, - setUnsavedChanges, lastSavedBrew, editorRef, - isSaving, - setIsSaving, save, - lastSavedTime, - setLastSavedTime } = dependencies; + const [isSaving, setIsSaving] = useState(false); + const [lastSavedTime, setLastSavedTime] = useState(new Date()); + const [autoSaveEnabled, setAutoSaveEnabled] = useState(!sandbox); + const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); + const [unsavedChanges, setUnsavedChanges] = useState(false); + const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time const saveTimeout = useRef(null); @@ -59,7 +57,6 @@ export default function useCommonEditPageFunctions(dependencies) { e.preventDefault(); } }; - document.addEventListener('keydown', handleControlKeys); window.onbeforeunload = ()=>{ if(unsavedChangesRef.current) @@ -145,12 +142,43 @@ export default function useCommonEditPageFunctions(dependencies) { }, newTimeout); }); + const renderSaveButton = ()=>{ + if(isSaving) + return saving...; + + if(unsavedChanges && warnUnsavedChanges) { + resetWarnUnsavedTimer(); + const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}.` + : `Autosave is OFF${sandbox ? ' for this sandbox page' : ''}, and you haven't saved for ${elapsedTime} minutes.`; + + return + Reminder... +
{text}
+
; + } + + if(unsavedChanges) + return trySave(true, true, saveGoogle)} color='blue' icon='fas fa-save'>save now; + + if(autoSaveEnabled) + return auto-saved; + + if(sandbox) + return save now; + + return saved; + }; + return { - resetWarnUnsavedTimer, handleSplitMove, handleBrewChange, toggleAutoSave, clearError, trySave, + renderSaveButton, + autoSaveEnabled, + unsavedChanges, } } \ No newline at end of file From 2676abe4e7c142bc7ca5a788b902d7fd92870462 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Mon, 14 Sep 2026 20:51:09 -0400 Subject: [PATCH 18/27] Change hook from js to jsx --- client/homebrew/pages/editPage/editPage.jsx | 2 +- client/homebrew/pages/homePage/homePage.jsx | 2 +- client/homebrew/pages/newPage/newPage.jsx | 2 +- ...onEditPageFunctions.js => commonEditPageFunctions.jsx} | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) rename client/homebrew/utils/{commonEditPageFunctions.js => commonEditPageFunctions.jsx} (95%) diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d8f550ff4..205cf3fcb 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -9,7 +9,7 @@ import _ from 'lodash'; import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; -import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx' import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index d571c74ea..0132a1147 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -9,7 +9,7 @@ import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; -import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx' import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index bbeb1ed52..48d589c1e 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -10,7 +10,7 @@ import _ from 'lodash'; import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js'; -import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.js' +import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx' import SplitPane from '@components/splitPane/splitPane.jsx'; import Editor from '../../editor/editor.jsx'; diff --git a/client/homebrew/utils/commonEditPageFunctions.js b/client/homebrew/utils/commonEditPageFunctions.jsx similarity index 95% rename from client/homebrew/utils/commonEditPageFunctions.js rename to client/homebrew/utils/commonEditPageFunctions.jsx index 89cd2e35e..c8ecbaa0c 100644 --- a/client/homebrew/utils/commonEditPageFunctions.js +++ b/client/homebrew/utils/commonEditPageFunctions.jsx @@ -30,11 +30,11 @@ export default function useCommonEditPageFunctions(dependencies) { save, } = dependencies; - const [isSaving, setIsSaving] = useState(false); - const [lastSavedTime, setLastSavedTime] = useState(new Date()); - const [autoSaveEnabled, setAutoSaveEnabled] = useState(!sandbox); + const [isSaving , setIsSaving] = useState(false); + const [lastSavedTime , setLastSavedTime] = useState(new Date()); + const [autoSaveEnabled , setAutoSaveEnabled] = useState(!sandbox); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true); - const [unsavedChanges, setUnsavedChanges] = useState(false); + const [unsavedChanges , setUnsavedChanges] = useState(false); const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time From 3e068129bbcd5bd0f9684f1e760e92b6ba6eecb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Wed, 16 Sep 2026 12:26:39 +0200 Subject: [PATCH 19/27] sum of all changes --- client/homebrew/navbar/navbar.jsx | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/client/homebrew/navbar/navbar.jsx b/client/homebrew/navbar/navbar.jsx index 40e5aef71..c55672d15 100644 --- a/client/homebrew/navbar/navbar.jsx +++ b/client/homebrew/navbar/navbar.jsx @@ -1,34 +1,28 @@ import './navbar.less'; import React from 'react'; -import createReactClass from 'create-react-class'; import Nav from './nav.jsx'; import PatreonNavItem from './patreon.navitem.jsx'; -const Navbar = createReactClass({ - displayName : 'Navbar', - getInitialState : function() { - return { - ver : global.version || '0.0.0' - }; - }, +const Navbar = ({ children })=>{ + const version = global.version || '0.0.0'; - render : function(){ - return + return ( +
The Homebrewery
- {`v${this.state.ver}`} + {`v${version}`} - {/*this.renderChromeWarning()*/} + {/* this.renderChromeWarning() */}
- {this.props.children} -
; - } -}); + {children} +
+ ); +}; -export default Navbar; +export default Navbar; \ No newline at end of file From 7a9e561742754e3325b579737404a8377e2013a8 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 18 Sep 2026 10:06:48 -0400 Subject: [PATCH 20/27] Change visiblePages and centerPage to separate states Allows small logic cleanup in handlePageVisibilityChange --- client/homebrew/brewRenderer/brewRenderer.jsx | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 65de45177..577d683f8 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -111,11 +111,12 @@ const BrewRenderer = (props)=>{ ...props }; + const [visiblePages, setVisiblePages] = useState([]); + const [centerPage , setCenterPage ] = useState(1); + const [state, setState] = useState({ isMounted : false, - visibility : 'hidden', - visiblePages : [], - centerPage : 1 + visibility : 'hidden' }); const [displayOptions, setDisplayOptions] = useState({ @@ -145,21 +146,17 @@ const BrewRenderer = (props)=>{ rawPages = props.text.split(PAGEBREAK_REGEX_V3); } - const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{ - setState((prevState)=>{ - const updatedVisiblePages = new Set(prevState.visiblePages); - if(!isCenter) - isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); - - return { - ...prevState, - visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b), - centerPage : isCenter ? pageNum : prevState.centerPage - }; + const handlePageVisibilityChange = (pageNum, isVisible, isCenter) => { + setVisiblePages(prev => { + const updatedVisiblePages = new Set(prev); + isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); + return [...updatedVisiblePages].sort((a, b) => a - b); }); - if(isCenter) + if (isCenter) { + setCenterPage(pageNum); props.onPageChange(pageNum); + } }; const isInView = (index)=>{ @@ -346,7 +343,7 @@ const BrewRenderer = (props)=>{
- 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> + 0 ? visiblePages : [centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> {/*render in iFrame so broken code doesn't crash the site.*/} Date: Fri, 18 Sep 2026 10:26:24 -0400 Subject: [PATCH 21/27] Remove unused brewRenderer props --- client/homebrew/brewRenderer/brewRenderer.jsx | 4 ---- client/homebrew/pages/editPage/editPage.jsx | 2 -- client/homebrew/pages/homePage/homePage.jsx | 3 +-- client/homebrew/pages/newPage/newPage.jsx | 2 -- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 577d683f8..0f02a50b4 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -23,7 +23,6 @@ import safeHTML from './safeHTML.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m; const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m; -const PAGE_HEIGHT = 1056; const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState'; @@ -100,11 +99,9 @@ const BrewRenderer = (props)=>{ text : '', style : '', renderer : 'legacy', - theme : '5ePHB', lang : '', errors : [], currentEditorCursorPageNum : 1, - currentEditorViewPageNum : 1, currentBrewRendererPageNum : 1, themeBundle : {}, onPageChange : ()=>{}, @@ -138,7 +135,6 @@ const BrewRenderer = (props)=>{ const mainRef = useRef(null); const pagesRef = useRef(null); - const urlRef = useRef(''); if(props.renderer == 'legacy') { rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY); diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 205cf3fcb..57667ca6e 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -289,12 +289,10 @@ const EditPage = (props)=>{ text={currentBrew.text} style={currentBrew.style} renderer={currentBrew.renderer} - theme={currentBrew.theme} themeBundle={themeBundle} errors={HTMLErrors} lang={currentBrew.lang} onPageChange={setCurrentBrewRendererPageNum} - currentEditorViewPageNum={currentEditorViewPageNum} currentEditorCursorPageNum={currentEditorCursorPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum} allowPrint={true} diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 0132a1147..0ac78f0e1 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -138,11 +138,10 @@ const HomePage =(props)=>{ text={currentBrew.text} style={currentBrew.style} renderer={currentBrew.renderer} + themeBundle={themeBundle} onPageChange={setCurrentBrewRendererPageNum} - currentEditorViewPageNum={currentEditorViewPageNum} currentEditorCursorPageNum={currentEditorCursorPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum} - themeBundle={themeBundle} /> diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 48d589c1e..c7b6c7160 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -183,12 +183,10 @@ const NewPage = (props)=>{ text={currentBrew.text} style={currentBrew.style} renderer={currentBrew.renderer} - theme={currentBrew.theme} themeBundle={themeBundle} errors={HTMLErrors} lang={currentBrew.lang} onPageChange={setCurrentBrewRendererPageNum} - currentEditorViewPageNum={currentEditorViewPageNum} currentEditorCursorPageNum={currentEditorCursorPageNum} currentBrewRendererPageNum={currentBrewRendererPageNum} allowPrint={true} From 0b6702c1988c2d383cad8fc50c5437829603f649 Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 18 Sep 2026 10:35:21 -0400 Subject: [PATCH 22/27] [brewRenderer] Remove mainRef; it does nothing --- 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 0f02a50b4..6776389c1 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -133,7 +133,6 @@ const BrewRenderer = (props)=>{ const [headerState, setHeaderState] = useState(false); - const mainRef = useRef(null); const pagesRef = useRef(null); if(props.renderer == 'legacy') { @@ -334,7 +333,7 @@ const BrewRenderer = (props)=>{ : null} -
+
From 90c42dc51d3a445fd074779fc96d49615eaca3ac Mon Sep 17 00:00:00 2001 From: Trevor Buckner Date: Fri, 18 Sep 2026 11:17:36 -0400 Subject: [PATCH 23/27] Linting --- client/homebrew/brewRenderer/brewRenderer.jsx | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 6776389c1..b198fa615 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -46,31 +46,25 @@ const BrewPage = (props)=>{ }; const pageRef = useRef(null); const cleanText = safeHTML(props.contents); + const pageNum = props.index + 1; useEffect(()=>{ if(!pageRef.current) return; - // Observer for tracking pages within the `.pages` div + // Observer for tracking which pages are at least 30% visible in the iframe const visibleObserver = new IntersectionObserver( - (entries)=>{ - entries.forEach((entry)=>{ - if(entry.isIntersecting) - props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages. - else - props.onVisibilityChange(props.index + 1, false, false); - }); - }, + (entries)=>entries.forEach((entry)=>{ + props.onVisibilityChange(pageNum, entry.isIntersecting, false); // add page to array of visible pages. + }), { threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds. ); // Observer for tracking the page at the center of the iframe. const centerObserver = new IntersectionObserver( - (entries)=>{ - entries.forEach((entry)=>{ - if(entry.isIntersecting) - props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page - }); - }, + (entries)=>entries.forEach((entry)=>{ + if(entry.isIntersecting) + props.onVisibilityChange(pageNum, true, true); // Set this page as the center page + }), { threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center ); @@ -91,7 +85,7 @@ const BrewPage = (props)=>{ //v=====--------------------< Brew Renderer Component >-------------------=====v// let renderedPages = []; -let pageTemplates = []; +const pageTemplates = []; let rawPages = []; const BrewRenderer = (props)=>{ @@ -109,11 +103,11 @@ const BrewRenderer = (props)=>{ }; const [visiblePages, setVisiblePages] = useState([]); - const [centerPage , setCenterPage ] = useState(1); + const [centerPage, setCenterPage] = useState(1); const [state, setState] = useState({ - isMounted : false, - visibility : 'hidden' + isMounted : false, + visibility : 'hidden' }); const [displayOptions, setDisplayOptions] = useState({ @@ -141,14 +135,14 @@ const BrewRenderer = (props)=>{ rawPages = props.text.split(PAGEBREAK_REGEX_V3); } - const handlePageVisibilityChange = (pageNum, isVisible, isCenter) => { - setVisiblePages(prev => { + const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{ + setVisiblePages((prev)=>{ const updatedVisiblePages = new Set(prev); isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum); - return [...updatedVisiblePages].sort((a, b) => a - b); + return [...updatedVisiblePages].sort((a, b)=>a - b); }); - if (isCenter) { + if(isCenter) { setCenterPage(pageNum); props.onPageChange(pageNum); } @@ -341,11 +335,11 @@ const BrewRenderer = (props)=>{ 0 ? visiblePages : [centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/> {/*render in iFrame so broken code doesn't crash the site.*/} - {emitClick();}} - sandbox="allow-same-origin allow-modals allow-top-navigation" + sandbox='allow-same-origin allow-modals allow-top-navigation' >
Date: Fri, 18 Sep 2026 11:24:39 -0400 Subject: [PATCH 24/27] Update brewRenderer.jsx --- 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 b198fa615..75a07c7d1 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -103,7 +103,7 @@ const BrewRenderer = (props)=>{ }; const [visiblePages, setVisiblePages] = useState([]); - const [centerPage, setCenterPage] = useState(1); + const [centerPage , setCenterPage ] = useState(1); const [state, setState] = useState({ isMounted : false, From 2723f3e96f4b5464d15f504d33caa8e1a959de25 Mon Sep 17 00:00:00 2001 From: "G.Ambatte" Date: Sun, 20 Sep 2026 15:48:29 +1200 Subject: [PATCH 25/27] Remove live reload functionality --- client/homebrew/homebrew.jsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index a9165a00f..716291764 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -1,6 +1,5 @@ import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers import './homebrew.less'; -import React, { useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router'; import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js'; @@ -46,18 +45,6 @@ const Homebrew = (props)=>{ global.config = config; global.enablev4 = enablev4; - useEffect(()=>{ - const eventSource = new EventSource('/stream'); - - eventSource.addEventListener('message', (evt)=>{ - const messageData = JSON.parse(evt.data); - - if(messageData.eventType == 'initStream'){ - console.log('window.location.reload()'); - } - }); - }, []); - const backgroundObject = ()=>{ if(config?.deployment || (config?.local && config?.development)) { const bgText = config?.deployment || 'Local'; From 84b47b37e23c254c62f982980d4ccae9c38af60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= <5ecleric.naturalcrit@gmail.com> Date: Sun, 20 Sep 2026 20:31:18 +0200 Subject: [PATCH 26/27] Update client/homebrew/pages/sharePage/sharePage.jsx --- client/homebrew/pages/sharePage/sharePage.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 7d662fb9a..d81b4d878 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -39,8 +39,8 @@ const SharePage = (props)=>{ document.addEventListener('keydown', handleControlKeys); fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme); + // listen for changes in the brew version const eventSource = new EventSource('/stream'); - eventSource.addEventListener('message', (evt)=>{ const messageData = JSON.parse(evt.data); From dfe8ebfe1bb1299399c35855d516e59674566d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= <5ecleric.naturalcrit@gmail.com> Date: Sun, 20 Sep 2026 20:32:29 +0200 Subject: [PATCH 27/27] Update server/app.js --- server/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/app.js b/server/app.js index 06efbe7db..714c4beba 100644 --- a/server/app.js +++ b/server/app.js @@ -1,4 +1,4 @@ -/*eslint max-lines: ["warn", {"max": 750, "skipBlankLines": true, "skipComments": true}]*/ +/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/ // Set working directory to project root import { dirname } from 'path'; import { fileURLToPath } from 'url';