diff --git a/shared/naturalcrit/splitPane/splitPane.jsx b/client/components/splitPane/splitPane.jsx similarity index 100% rename from shared/naturalcrit/splitPane/splitPane.jsx rename to client/components/splitPane/splitPane.jsx diff --git a/shared/naturalcrit/splitPane/splitPane.less b/client/components/splitPane/splitPane.less similarity index 100% rename from shared/naturalcrit/splitPane/splitPane.less rename to client/components/splitPane/splitPane.less diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 6bcfc87ec..7a101e9f9 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -39,8 +39,8 @@ const BrewPage = (props)=>{ index : 0, ...props }; - const pageRef = useRef(null); - const cleanText = safeHTML(`${props.contents}\n
\n`); + const pageRef = useRef(null); + const cleanText = safeHTML(props.contents); useEffect(()=>{ if(!pageRef.current) return; diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx index ec72ace7d..6d9bec444 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -1,157 +1,138 @@ require('./error-navitem.less'); const React = require('react'); const Nav = require('naturalcrit/nav/nav.jsx'); -const createClass = require('create-react-class'); -const ErrorNavItem = createClass({ - getDefaultProps : function() { - return { - error : '', - parent : null - }; - }, - render : function() { - const clearError = ()=>{ - const state = { - error : null - }; - if(this.props.parent.state.isSaving) { - state.isSaving = false; - } - this.props.parent.setState(state); - }; +const ErrorNavItem = ({error = '', clearError})=>{ + const response = error.response; + const errorCode = error.code + const status = response?.status; + const HBErrorCode = response?.body?.HBErrorCode; + const message = response?.body?.message; - const error = this.props.error; - const response = error.response; - const status = response?.status; - const errorCode = error.code - const HBErrorCode = response?.body?.HBErrorCode; - const message = response?.body?.message; - let errMsg = ''; - try { - errMsg += `${error.toString()}\n\n`; - errMsg += `\`\`\`\n${error.stack}\n`; - errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``; - console.log(errMsg); - } catch (e){} - - if(status === 409) { - return - Oops! -
- {message ?? 'Conflict: please refresh to get latest changes'} -
-
; - } - - if(status === 412) { - return - Oops! -
- {message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'} -
-
; - } - - if(HBErrorCode === '04') { - return - Oops! -
- You are no longer signed in as an author of - this brew! Were you signed out from a different - window? Visit our log in page, then try again! -

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

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

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

+ +
+ Sign In +
+
+
+ Not Now +
+
+
; + } + + if(HBErrorCode === '09') { + return + Oops! +
+ Looks like there was a problem retreiving + the theme, or a theme that it inherits, + for this brew. Verify that brew + {response.body.brewId} still exists! +
+
; + } + + if(HBErrorCode === '10') { + return + Oops! +
+ Looks like the brew you have selected + as a theme is not tagged for use as a + theme. Verify that + brew + {response.body.brewId} has the meta:theme tag! +
+
; + } + + if(errorCode === 'ECONNABORTED') { + return + Oops! +
+ The request to the server was interrupted or timed out. + This can happen due to a network issue, or if + trying to save a particularly large brew. + Please check your internet connection and try again. +
+
; + } + + return + Oops! +
+ Looks like there was a problem saving.
+ Report the issue + here + . +
+
; +}; module.exports = ErrorNavItem; diff --git a/client/homebrew/navbar/newbrew.navitem.jsx b/client/homebrew/navbar/newbrew.navitem.jsx index 30d53c675..ccade4e8b 100644 --- a/client/homebrew/navbar/newbrew.navitem.jsx +++ b/client/homebrew/navbar/newbrew.navitem.jsx @@ -5,33 +5,45 @@ const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // const BREWKEY = 'homebrewery-new'; const STYLEKEY = 'homebrewery-new-style'; -const METAKEY = 'homebrewery-new-meta'; +const METAKEY = 'homebrewery-new-meta'; const NewBrew = ()=>{ const handleFileChange = (e)=>{ const file = e.target.files[0]; - if(file) { - const reader = new FileReader(); - reader.onload = (e)=>{ - const fileContent = e.target.result; - const newBrew = { - text : fileContent, - style : '' - }; - if(fileContent.startsWith('```metadata')) { - splitTextStyleAndMetadata(newBrew); // Modify newBrew directly - localStorage.setItem(BREWKEY, newBrew.text); - localStorage.setItem(STYLEKEY, newBrew.style); - localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']))); - window.location.href = '/new'; - } else { - alert('This file is invalid, please, enter a valid file'); - } - }; - reader.readAsText(file); - } + if(!file) return; + + const currentNew = localStorage.getItem(BREWKEY); + if(currentNew && !confirm( + `You have some text in the new brew space, if you load a file that text will be lost, are you sure you want to load the file?` + )) return; + + const reader = new FileReader(); + reader.onload = (e)=>{ + const fileContent = e.target.result; + const newBrew = { text: fileContent, style: '' }; + + if(fileContent.startsWith('```metadata')) { + splitTextStyleAndMetadata(newBrew); + localStorage.setItem(BREWKEY, newBrew.text); + localStorage.setItem(STYLEKEY, newBrew.style); + localStorage.setItem(METAKEY, JSON.stringify( + _.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']) + )); + window.location.href = '/new'; + return; + } + + const type = file.name.split('.').pop().toLowerCase(); + + alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`); + + + console.log(file); + }; + reader.readAsText(file); }; + return ( ( + brew.googleId && !brew.stubbed + ? brew.googleId + brew.shareId + : brew.shareId + ); + + const getRedditLink = (brew)=>{ + const text = dedent` + Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. + + **[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`; + + return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`; + }; + +export default ({brew}) => ( + + + share + + + view + + {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}> + copy url + + + post to reddit + + +); diff --git a/client/homebrew/pages/basePages/uiPage/uiPage.less b/client/homebrew/pages/basePages/uiPage/uiPage.less index 39ccf1d74..f00b484c0 100644 --- a/client/homebrew/pages/basePages/uiPage/uiPage.less +++ b/client/homebrew/pages/basePages/uiPage/uiPage.less @@ -29,6 +29,7 @@ &::before { margin-right : 5px; font-family : 'Font Awesome 6 Free'; + font-weight : 900; content : '\f00c'; } } diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 29dad9de0..6c2220ec1 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,529 +1,418 @@ /* eslint-disable max-lines */ -require('./editPage.less'); -const React = require('react'); -const _ = require('lodash'); -const createClass = require('create-react-class'); -import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; +import './editPage.less'; -import request from '../../utils/request-middleware.js'; -const { Meta } = require('vitreum/headtags'); +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import request from '../../utils/request-middleware.js'; +import Markdown from 'naturalcrit/markdown.js'; -const Nav = require('naturalcrit/nav/nav.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); +import _ from 'lodash';; +import { makePatches, stringifyPatches } from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; +import { Meta } from 'vitreum/headtags'; -const NewBrew = require('../../navbar/newbrew.navitem.jsx'); -const HelpNavItem = require('../../navbar/help.navitem.jsx'); -const PrintNavItem = require('../../navbar/print.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const Account = require('../../navbar/account.navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const VaultNavItem = require('../../navbar/vault.navitem.jsx'); +import Nav from 'naturalcrit/nav/nav.jsx'; +import Navbar from '../../navbar/navbar.jsx'; +import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; +import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ShareNavItem from '../../navbar/share.navitem.jsx'; +import ErrorNavItem from '../../navbar/error-navitem.jsx'; +import HelpNavItem from '../../navbar/help.navitem.jsx'; +import VaultNavItem from '../../navbar/vault.navitem.jsx'; +import PrintNavItem from '../../navbar/print.navitem.jsx'; +import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; -const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -const LockNotification = require('./lockNotification/lockNotification.jsx'); +import LockNotification from './lockNotification/lockNotification.jsx'; -import Markdown from 'naturalcrit/markdown.js'; - -const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); -const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); +import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle } from '../../../../shared/helpers.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; -const googleDriveIcon = require('../../googleDrive.svg'); +import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; +const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes +const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds -const EditPage = createClass({ - displayName : 'EditPage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW_LOAD - }; - }, +const EditPage = (props)=>{ + props = { + brew : DEFAULT_BREW_LOAD, + ...props + }; - getInitialState : function() { - return { - brew : this.props.brew, - isSaving : false, - unsavedChanges : false, - alertTrashedGoogleBrew : this.props.brew.trashed, - alertLoginToTransfer : false, - saveGoogle : this.props.brew.googleId ? true : false, - confirmGoogleTransfer : false, - error : null, - htmlErrors : Markdown.validate(this.props.brew.text), - url : '', - autoSave : true, - autoSaveWarning : false, - unsavedTime : new Date(), - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - displayLockMessage : this.props.brew.lock || false, - themeBundle : {} - }; - }, + const [currentBrew , setCurrentBrew ] = useState(props.brew); + const [isSaving , setIsSaving ] = useState(false); + const [lastSavedTime , setLastSavedTime ] = useState(new Date()); + const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); + const [error , setError ] = useState(null); + const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); + const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); + const [themeBundle , setThemeBundle ] = useState({}); + const [unsavedChanges , setUnsavedChanges ] = useState(false); + const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed); + const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); + const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); + const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); + const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true); - editor : React.createRef(null), - savedBrew : null, + const editorRef = useRef(null); + const lastSavedBrew = useRef(_.cloneDeep(props.brew)); + const saveTimeout = useRef(null); + const warnUnsavedTimeout = useRef(null); + const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew + const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges - componentDidMount : function(){ - this.setState({ - url : window.location.href - }); + useEffect(()=>{ + const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); + setAutoSaveEnabled(autoSavePref); + setWarnUnsavedChanges(!autoSavePref); + setHTMLErrors(Markdown.validate(currentBrew.text)); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy - - this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{ - if(this.state.autoSave){ - this.trySave(); - } else { - this.setState({ autoSaveWarning: true }); + const handleControlKeys = (e)=>{ + if(!(e.ctrlKey || e.metaKey)) return; + if(e.keyCode === 83) trySaveRef.current(true); + if(e.keyCode === 80) printCurrentBrew(); + if([83, 80].includes(e.keyCode)) { + e.stopPropagation(); + e.preventDefault(); } - }); + }; + document.addEventListener('keydown', handleControlKeys); window.onbeforeunload = ()=>{ - if(this.state.isSaving || this.state.unsavedChanges){ + if(unsavedChangesRef.current) return 'You have unsaved changes!'; - } }; + return ()=>{ + document.removeEventListener('keydown', handleControlKeys); + window.onBeforeUnload = null; + }; + }, []); - this.setState((prevState)=>({ - htmlErrors : Markdown.validate(prevState.brew.text) - })); + useEffect(()=>{ + trySaveRef.current = trySave; + unsavedChangesRef.current = unsavedChanges; + }); - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); + useEffect(()=>{ + const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current); + setUnsavedChanges(hasChange); - document.addEventListener('keydown', this.handleControlKeys); - }, - componentWillUnmount : function() { - window.onbeforeunload = function(){}; - document.removeEventListener('keydown', this.handleControlKeys); - }, - componentDidUpdate : function(){ - const hasChange = this.hasChanges(); - if(this.state.unsavedChanges != hasChange){ - this.setState({ - unsavedChanges : hasChange - }); - } - }, + if(autoSaveEnabled) trySave(false, hasChange); + }, [currentBrew]); - handleControlKeys : function(e){ - if(!(e.ctrlKey || e.metaKey)) return; - const S_KEY = 83; - const P_KEY = 80; - if(e.keyCode == S_KEY) this.trySave(true); - if(e.keyCode == P_KEY) printCurrentBrew(); - if(e.keyCode == P_KEY || e.keyCode == S_KEY){ - e.stopPropagation(); - e.preventDefault(); - } - }, + const handleSplitMove = ()=>{ + editorRef.current?.update(); + }; - handleSplitMove : function(){ - this.editor.current.update(); - }, + const handleEditorViewPageChange = (pageNumber)=>{ + setCurrentEditorViewPageNum(pageNumber); + }; - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, + const handleEditorCursorPageChange = (pageNumber)=>{ + setCurrentEditorCursorPageNum(pageNumber); + }; - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + const handleBrewRendererPageChange = (pageNumber)=>{ + setCurrentBrewRendererPageNum(pageNumber); + }; - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: pageNumber }); - }, + const handleTextChange = (text)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + setHTMLErrors(Markdown.validate(text)); + setCurrentBrew((prevBrew)=>({ ...prevBrew, text })); + }; - handleTextChange : function(text){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(text); + const handleStyleChange = (style)=>{ + setCurrentBrew((prevBrew)=>({ ...prevBrew, style })); + }; - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + const handleSnipChange = (snippet)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + setHTMLErrors(Markdown.validate(snippet)); + setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet })); + }; - handleSnipChange : function(snippet){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + const handleMetaChange = (metadata, field = undefined)=>{ + if(field === 'theme' || field === 'renderer') + fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - this.setState((prevState)=>({ - brew : { ...prevState.brew, snippets: snippet }, - unsavedChanges : true, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setCurrentBrew((prev)=>({ ...prev, ...metadata })); + }; - handleStyleChange : function(style){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({ + ...prevBrew, + style : newData.style, + text : newData.text, + snippets : newData.snippets + })); - handleMetaChange : function(metadata, field=undefined){ - if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle(this, metadata.renderer, metadata.theme); + 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 + }; - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - ...metadata - } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, - - hasChanges : function(){ - return !_.isEqual(this.state.brew, this.savedBrew); - }, - - updateBrew : function(newData){ - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - style : newData.style, - text : newData.text, - snippets : newData.snippets - } - })); - }, - - trySave : function(immediate=false){ - if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); - if(this.state.isSaving) - return; - - if(immediate) { - this.debounceSave(); - this.debounceSave.flush(); - return; - } - - if(this.hasChanges()) - this.debounceSave(); - else - this.debounceSave.cancel(); - }, - - handleGoogleClick : function(){ + const handleGoogleClick = ()=>{ if(!global.account?.googleId) { - this.setState({ - alertLoginToTransfer : true - }); + setAlertLoginToTransfer(true); return; } - this.setState((prevState)=>({ - confirmGoogleTransfer : !prevState.confirmGoogleTransfer - })); - this.setState({ - error : null - }); - }, - closeAlerts : function(event){ - event.stopPropagation(); //Only handle click once so alert doesn't reopen - this.setState({ - alertTrashedGoogleBrew : false, - alertLoginToTransfer : false, - confirmGoogleTransfer : false - }); - }, + setConfirmGoogleTransfer((prev)=>!prev); + setError(null); + }; - toggleGoogleStorage : function(){ - this.setState((prevState)=>({ - saveGoogle : !prevState.saveGoogle, - error : null - }), ()=>this.trySave(true)); - }, + const closeAlerts = (e)=>{ + e.stopPropagation(); //Only handle click once so alert doesn't reopen + setAlertTrashedGoogleBrew(false); + setAlertLoginToTransfer(false); + setConfirmGoogleTransfer(false); + }; - save : async function(){ - if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); + const toggleGoogleStorage = ()=>{ + setSaveGoogle((prev)=>!prev); + setError(null); + trySave(true); + }; - const brewState = this.state.brew; // freeze the current state - const preSaveSnapshot = { ...brewState }; + const trySave = (immediate = false, hasChanges = true)=>{ + clearTimeout(saveTimeout.current); + if(isSaving) return; + if(!hasChanges && !immediate) return; + const newTimeout = immediate ? 0 : SAVE_TIMEOUT; - this.setState((prevState)=>({ - isSaving : true, - error : null, - htmlErrors : Markdown.validate(prevState.brew.text) - })); + saveTimeout.current = setTimeout(async ()=>{ + setIsSaving(true); + setError(null); + await save(currentBrew, saveGoogle) + .catch((err)=>{ + setError(err); + }); + setIsSaving(false); + setLastSavedTime(new Date()); + if(!autoSaveEnabled) resetWarnUnsavedTimer(); + }, newTimeout); + }; - await updateHistory(this.state.brew).catch(console.error); + const save = async (brew, saveToGoogle)=>{ + setHTMLErrors(Markdown.validate(brew.text)); + + await updateHistory(brew).catch(console.error); await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server - const brew = { ...brewState }; - brew.text = brew.text.normalize('NFC'); - this.savedBrew.text = this.savedBrew.text.normalize('NFC'); - brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; - brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text))); - brew.hash = await md5(this.savedBrew.text); - //brew.text = undefined; - Temporary parallel path - brew.textBin = undefined; + const brewToSave = { + ...brew, + text : brew.text.normalize('NFC'), + pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))), + hash : await md5(lastSavedBrew.current.text), + textBin : undefined, + version : lastSavedBrew.current.version + }; - const compressedBrew = gzipSync(strToU8(JSON.stringify(brew))); + const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); + const transfer = saveToGoogle === _.isNil(brew.googleId); + const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; - const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); - const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; const res = await request - .put(`/api/update/${brew.editId}${params}`) + .put(`/api/update/${brewToSave.editId}${params}`) .set('Content-Encoding', 'gzip') .set('Content-Type', 'application/json') .send(compressedBrew) .catch((err)=>{ - console.log('Error Updating Local Brew'); - this.setState({ error: err }); + console.error('Error Updating Local Brew'); + setError(err); }); if(!res) return; - this.savedBrew = { - ...preSaveSnapshot, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, + const updatedFields = { + googleId : res.body.googleId ?? null, + editId : res.body.editId, shareId : res.body.shareId, version : res.body.version }; - this.setState((prevState) => ({ - brew: { - ...prevState.brew, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version - }, - isSaving : false, - unsavedTime : new Date() - }), ()=>{ - this.setState({ unsavedChanges : this.hasChanges() }); - }); + lastSavedBrew.current = { + ...brew, + ...updatedFields + }; - history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); - }, + setCurrentBrew((prevBrew)=>({ + ...prevBrew, + ...updatedFields + })); - renderGoogleDriveIcon : function(){ - return - Google Drive icon + history.replaceState(null, null, `/edit/${res.body.editId}`); + }; - {this.state.confirmGoogleTransfer && -
- { this.state.saveGoogle - ? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?` - : `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?` - } + const renderGoogleDriveIcon = ()=>( + + Google Drive icon + + {confirmGoogleTransfer && ( +
+ {saveGoogle + ? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?' + : 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
-
- Yes -
-
- No -
+
Yes
+
No
- } + )} - {this.state.alertLoginToTransfer && -
- You must be signed in to a Google account to transfer - between the homebrewery and Google Drive! - -
- Sign In -
+ {alertLoginToTransfer && ( +
- } + )} - {this.state.alertTrashedGoogleBrew && -
- This brew is currently in your Trash folder on Google Drive!
If you want to keep it, make sure to move it before it is deleted permanently!
-
- OK -
+ {alertTrashedGoogleBrew && ( +
+ This brew is currently in your Trash folder on Google Drive!
+ If you want to keep it, make sure to move it before it is deleted permanently!
+
OK
- } - ; - }, - - renderSaveButton : function(){ + )} + + ); + const renderSaveButton = ()=>{ // #1 - Currently saving, show SAVING - if(this.state.isSaving){ + if(isSaving) return saving...; - } // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(this.state.unsavedChanges && this.state.autoSaveWarning){ - this.setAutosaveWarning(); - const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60); - const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + if(unsavedChanges && warnUnsavedChanges) { + resetWarnUnsavedTimer(); + const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? 'Autosave is OFF.' + : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; return - Reminder... -
- {text} -
+ Reminder... +
{text}
; } // #3 - Unsaved changes exist, click to save, show SAVE NOW - // Use trySave(true) instead of save() to use debounced save function - if(this.state.unsavedChanges){ - return this.trySave(true)} color='blue' icon='fas fa-save'>Save Now; - } + if(unsavedChanges) + return trySave(true)} color='blue' icon='fas fa-save'>Save Now; + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(this.state.autoSave){ + if(autoSaveEnabled) return auto-saved.; - } + // DEFAULT - No unsaved changes, show SAVED return saved.; - }, + }; - handleAutoSave : function(){ - if(this.warningTimer) clearTimeout(this.warningTimer); - this.setState((prevState)=>({ - autoSave : !prevState.autoSave, - autoSaveWarning : prevState.autoSave - }), ()=>{ - localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave)); - }); - }, + const toggleAutoSave = ()=>{ + clearTimeout(warnUnsavedTimeout.current); + clearTimeout(saveTimeout.current); + localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); + setAutoSaveEnabled(!autoSaveEnabled); + setWarnUnsavedChanges(autoSaveEnabled); + }; - setAutosaveWarning : function(){ - setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display - this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings - this.warningTimer; - }, + const renderAutoSaveButton = ()=>( + + Autosave + + ); - errorReported : function(error) { - this.setState({ - error - }); - }, - - renderAutoSaveButton : function(){ - return - Autosave - ; - }, - - processShareId : function() { - return this.state.brew.googleId && !this.state.brew.stubbed ? - this.state.brew.googleId + this.state.brew.shareId : - this.state.brew.shareId; - }, - - getRedditLink : function(){ - - const shareLink = this.processShareId(); - const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : ''; - const title = `${this.props.brew.title} ${systems}`; - const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. - -**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; - - return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; - }, - - renderNavbar : function(){ - const shareLink = this.processShareId(); + const clearError = ()=>{ + setError(null); + setIsSaving(false); + }; + const renderNavbar = ()=>{ return - {this.state.brew.title} + {currentBrew.title} - {this.renderGoogleDriveIcon()} - {this.state.error ? - : - - {this.renderSaveButton()} - {this.renderAutoSaveButton()} - - } - + {renderGoogleDriveIcon()} + {error + ? + : + {renderSaveButton()} + {renderAutoSaveButton()} + } + - - - share - - - view - - {navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}> - copy url - - - post to reddit - - + - - + + - ; - }, + }; - render : function(){ - return
+ return ( +
- {this.renderNavbar()} - {this.props.brew.lock && } + {renderNavbar()} + + {currentBrew.lock && } +
- +
-
; - } -}); +
+ ); +}; module.exports = EditPage; diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index d03e30c91..84967b1ff 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -1,90 +1,91 @@ -require('./homePage.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const cx = require('classnames'); -import request from '../../utils/request-middleware.js'; -const { Meta } = require('vitreum/headtags'); +import './homePage.less'; -const Nav = require('naturalcrit/nav/nav.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); -const NewBrewItem = require('../../navbar/newbrew.navitem.jsx'); -const HelpNavItem = require('../../navbar/help.navitem.jsx'); -const VaultNavItem = require('../../navbar/vault.navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const AccountNavItem = require('../../navbar/account.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const { fetchThemeBundle } = require('../../../../shared/helpers.js'); +import React from 'react'; +import { useEffect, useState, useRef } from 'react'; +import request from '../../utils/request-middleware.js'; +import { Meta } from 'vitreum/headtags'; -const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import Nav from 'naturalcrit/nav/nav.jsx'; +import Navbar from '../../navbar/navbar.jsx'; +import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; +import HelpNavItem from '../../navbar/help.navitem.jsx'; +import VaultNavItem from '../../navbar/vault.navitem.jsx'; +import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; +import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ErrorNavItem from '../../navbar/error-navitem.jsx'; +import { fetchThemeBundle } from '../../../../shared/helpers.js'; -const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -const HomePage = createClass({ - displayName : 'HomePage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW, - ver : '0.0.0' - }; - }, - getInitialState : function() { - return { - brew : this.props.brew, - welcomeText : this.props.brew.text, - error : undefined, - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - themeBundle : {} - }; - }, +import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; - editor : React.createRef(null), +const HomePage =(props)=>{ + props = { + brew : DEFAULT_BREW, + ver : '0.0.0', + ...props + }; - componentDidMount : function() { - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); - }, + const [brew , setBrew] = useState(props.brew); + const [welcomeText , setWelcomeText] = useState(props.brew.text); + const [error , setError] = useState(undefined); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); + const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); + const [themeBundle , setThemeBundle] = useState({}); + const [isSaving , setIsSaving] = useState(false); - handleSave : function(){ + const editorRef = useRef(null); + + useEffect(()=>{ + fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme); + }, []); + + const save = ()=>{ request.post('/api') - .send(this.state.brew) + .send(brew) .end((err, res)=>{ if(err) { - this.setState({ error: err }); + setError(err); return; } - const brew = res.body; - window.location = `/edit/${brew.editId}`; + const saved = res.body; + window.location = `/edit/${saved.editId}`; }); - }, - handleSplitMove : function(){ - this.editor.current.update(); - }, + }; - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, + const handleSplitMove = ()=>{ + editorRef.current.update(); + }; - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + const handleEditorViewPageChange = (pageNumber)=>{ + setCurrentEditorViewPageNum(pageNumber); + }; + + const handleEditorCursorPageChange = (pageNumber)=>{ + setCurrentEditorCursorPageNum(pageNumber); + }; + + const handleBrewRendererPageChange = (pageNumber)=>{ + setCurrentBrewRendererPageNum(pageNumber); + }; - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: pageNumber }); - }, + const handleTextChange = (text)=>{ + setBrew((prevBrew) => ({ ...prevBrew, text })); + }; - handleTextChange : function(text){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - })); - }, - renderNavbar : function(){ - return + const clearError = ()=>{ + setError(null); + setIsSaving(false); + }; + + const renderNavbar = ()=>{ + return - {this.state.error ? - : + {error ? + : null } @@ -94,48 +95,48 @@ const HomePage = createClass({ ; - }, + }; - render : function(){ - return
+ return ( +
- {this.renderNavbar()} + {renderNavbar()}
- +
-
+
Save current
Create your own -
; - } -}); +
+ ) +}; module.exports = HomePage; diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 64fac86c0..bb21441cf 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -1,275 +1,251 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ -require('./newPage.less'); -const React = require('react'); -const createClass = require('create-react-class'); -import request from '../../utils/request-middleware.js'; +import './newPage.less'; -import Markdown from 'naturalcrit/markdown.js'; +import React, { useState, useEffect, useRef } from 'react'; +import request from '../../utils/request-middleware.js'; +import Markdown from 'naturalcrit/markdown.js'; -const Nav = require('naturalcrit/nav/nav.jsx'); -const PrintNavItem = require('../../navbar/print.navitem.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); -const AccountNavItem = require('../../navbar/account.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const HelpNavItem = require('../../navbar/help.navitem.jsx'); +import Nav from 'naturalcrit/nav/nav.jsx'; +import Navbar from '../../navbar/navbar.jsx'; +import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ErrorNavItem from '../../navbar/error-navitem.jsx'; +import HelpNavItem from '../../navbar/help.navitem.jsx'; +import PrintNavItem from '../../navbar/print.navitem.jsx'; +import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; -const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js'); -const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); +import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; const BREWKEY = 'homebrewery-new'; const STYLEKEY = 'homebrewery-new-style'; const METAKEY = 'homebrewery-new-meta'; -let SAVEKEY; +const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; +const NewPage = (props) => { + props = { + brew: DEFAULT_BREW, + ...props + }; -const NewPage = createClass({ - displayName : 'NewPage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW + const [currentBrew , setCurrentBrew ] = useState(props.brew); + const [isSaving , setIsSaving ] = useState(false); + const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false); + const [error , setError ] = useState(null); + const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); + const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); + const [themeBundle , setThemeBundle ] = useState({}); + + const editorRef = useRef(null); + + useEffect(() => { + document.addEventListener('keydown', handleControlKeys); + loadBrew(); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); + + return () => { + document.removeEventListener('keydown', handleControlKeys); }; - }, + }, []); - getInitialState : function() { - const brew = this.props.brew; - - return { - brew : brew, - isSaving : false, - saveGoogle : (global.account && global.account.googleId ? true : false), - error : null, - htmlErrors : Markdown.validate(brew.text), - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - themeBundle : {} - }; - }, - - editor : React.createRef(null), - - componentDidMount : function() { - document.addEventListener('keydown', this.handleControlKeys); - - const brew = this.state.brew; - - if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser + const loadBrew = ()=>{ + const brew = { ...currentBrew }; + if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser const brewStorage = localStorage.getItem(BREWKEY); const styleStorage = localStorage.getItem(STYLEKEY); - const metaStorage = JSON.parse(localStorage.getItem(METAKEY)); + const metaStorage = JSON.parse(localStorage.getItem(METAKEY)); - brew.text = brewStorage ?? brew.text; - brew.style = styleStorage ?? brew.style; - // brew.title = metaStorage?.title || this.state.brew.title; - // brew.description = metaStorage?.description || this.state.brew.description; + brew.text = brewStorage ?? brew.text; + brew.style = styleStorage ?? brew.style; brew.renderer = metaStorage?.renderer ?? brew.renderer; brew.theme = metaStorage?.theme ?? brew.theme; brew.lang = metaStorage?.lang ?? brew.lang; } - SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`; const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY'; - this.setState({ - brew : brew, - saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle) - }); - - fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme); + setCurrentBrew(brew); + setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle); localStorage.setItem(BREWKEY, brew.text); if(brew.style) localStorage.setItem(STYLEKEY, brew.style); - localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang })); - if(window.location.pathname != '/new') { + localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang })); + if(window.location.pathname !== '/new') window.history.replaceState({}, window.location.title, '/new/'); - } - }, - componentWillUnmount : function() { - document.removeEventListener('keydown', this.handleControlKeys); - }, + }; - handleControlKeys : function(e){ - if(!(e.ctrlKey || e.metaKey)) return; + const handleControlKeys = (e) => { + if (!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; - if(e.keyCode == S_KEY) this.save(); - if(e.keyCode == P_KEY) printCurrentBrew(); - if(e.keyCode == P_KEY || e.keyCode == S_KEY){ - e.stopPropagation(); + if (e.keyCode === S_KEY) save(); + if (e.keyCode === P_KEY) printCurrentBrew(); + if (e.keyCode === S_KEY || e.keyCode === P_KEY) { e.preventDefault(); + e.stopPropagation(); } - }, + }; - handleSplitMove : function(){ - this.editor.current.update(); - }, + const handleSplitMove = ()=>{ + editorRef.current.update(); + }; - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, + const handleEditorViewPageChange = (pageNumber)=>{ + setCurrentEditorViewPageNum(pageNumber); + }; + + const handleEditorCursorPageChange = (pageNumber)=>{ + setCurrentEditorCursorPageNum(pageNumber); + }; + + const handleBrewRendererPageChange = (pageNumber)=>{ + setCurrentBrewRendererPageNum(pageNumber); + }; - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + const handleTextChange = (text)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(text); - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: pageNumber }); - }, - - handleTextChange : function(text){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(text); - - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - })); + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); localStorage.setItem(BREWKEY, text); - }, + }; - handleStyleChange : function(style){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style }, - })); + const handleStyleChange = (style) => { + setCurrentBrew(prevBrew => ({ ...prevBrew, style })); localStorage.setItem(STYLEKEY, style); - }, + }; - handleSnipChange : function(snippet){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + const handleSnipChange = (snippet)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(snippet); - this.setState((prevState)=>({ - brew : { ...prevState.brew, snippets: snippet }, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); + }; - handleMetaChange : function(metadata, field=undefined){ - if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle(this, metadata.renderer, metadata.theme); + const handleMetaChange = (metadata, field = undefined) => { + if (field === 'theme' || field === 'renderer') + fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - this.setState((prevState)=>({ - brew : { ...prevState.brew, ...metadata }, - }), ()=>{ - localStorage.setItem(METAKEY, JSON.stringify({ - // 'title' : this.state.brew.title, - // 'description' : this.state.brew.description, - 'renderer' : this.state.brew.renderer, - 'theme' : this.state.brew.theme, - 'lang' : this.state.brew.lang - })); - }); - ; - }, + setCurrentBrew(prev => ({ ...prev, ...metadata })); + localStorage.setItem(METAKEY, JSON.stringify({ + renderer : metadata.renderer, + theme : metadata.theme, + lang : metadata.lang + })); + }; - save : async function(){ - this.setState({ - isSaving : true - }); + const save = async () => { + setIsSaving(true); - let brew = this.state.brew; - // Split out CSS to Style if CSS codefence exists - if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) { - const index = brew.text.indexOf('```\n\n'); - brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`; - brew.text = brew.text.slice(index + 5); - } + let updatedBrew = { ...currentBrew }; + splitTextStyleAndMetadata(updatedBrew); + + const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm; + updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1; - brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; const res = await request - .post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`) - .send(brew) - .catch((err)=>{ - this.setState({ isSaving: false, error: err }); + .post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`) + .send(updatedBrew) + .catch((err) => { + setIsSaving(false); + setError(err); }); - if(!res) return; - brew = res.body; + setIsSaving(false) + if (!res) return; + + const savedBrew = res.body; + localStorage.removeItem(BREWKEY); localStorage.removeItem(STYLEKEY); localStorage.removeItem(METAKEY); - window.location = `/edit/${brew.editId}`; - }, + window.location = `/edit/${savedBrew.editId}`; + }; - renderSaveButton : function(){ - if(this.state.isSaving){ + const renderSaveButton = ()=>{ + if(isSaving){ return save... ; } else { - return + return save ; } - }, + }; - renderNavbar : function(){ - return + const clearError = ()=>{ + setError(null); + setIsSaving(false); + }; + const renderNavbar = () => ( + - {this.state.brew.title} + {currentBrew.title} - {this.state.error ? - : - this.renderSaveButton() - } + {error + ? + : renderSaveButton()} - ; - }, + + ); - render : function(){ - return
- {this.renderNavbar()} + return ( +
+ {renderNavbar()}
- +
-
; - } -}); +
+ ); +}; module.exports = NewPage; diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index e9c5540a2..50104a665 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -17,15 +17,11 @@ const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpe const SharePage = (props)=>{ const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props; - const [state, setState] = useState({ - themeBundle : {}, - currentBrewRendererPageNum : 1, - }); + const [themeBundle, setThemeBundle] = useState({}); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const handleBrewRendererPageChange = useCallback((pageNumber)=>{ - setState((prevState)=>({ - currentBrewRendererPageNum : pageNumber, - ...prevState })); + setCurrentBrewRendererPageNum(pageNumber); }, []); const handleControlKeys = (e)=>{ @@ -40,11 +36,7 @@ const SharePage = (props)=>{ useEffect(()=>{ document.addEventListener('keydown', handleControlKeys); - fetchThemeBundle( - { setState }, - brew.renderer, - brew.theme - ); + fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme); return ()=>{ document.removeEventListener('keydown', handleControlKeys); @@ -114,9 +106,9 @@ const SharePage = (props)=>{ lang={brew.lang} renderer={brew.renderer} theme={brew.theme} - themeBundle={state.themeBundle} + themeBundle={themeBundle} onPageChange={handleBrewRendererPageChange} - currentBrewRendererPageNum={state.currentBrewRendererPageNum} + currentBrewRendererPageNum={currentBrewRendererPageNum} allowPrint={true} />
diff --git a/client/homebrew/pages/userPage/userPage.jsx b/client/homebrew/pages/userPage/userPage.jsx index f6fae639d..e4a8b0b4e 100644 --- a/client/homebrew/pages/userPage/userPage.jsx +++ b/client/homebrew/pages/userPage/userPage.jsx @@ -39,10 +39,14 @@ const UserPage = (props)=>{ }] : []) ]; + const clearError = ()=>{ + setError(null); + }; + const navItems = ( - {error && ()} + {error && ()} diff --git a/client/homebrew/pages/vaultPage/vaultPage.jsx b/client/homebrew/pages/vaultPage/vaultPage.jsx index f979aa4f7..e098bb1a2 100644 --- a/client/homebrew/pages/vaultPage/vaultPage.jsx +++ b/client/homebrew/pages/vaultPage/vaultPage.jsx @@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx'); const NewBrew = require('../../navbar/newbrew.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx'); const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx'); -const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx'); +const SplitPane = require('client/components/splitPane/splitPane.jsx'); const ErrorIndex = require('../errorPage/errors/errorIndex.js'); import request from '../../utils/request-middleware.js'; diff --git a/client/homebrew/utils/request-middleware.spec.js b/client/homebrew/utils/request-middleware.spec.js new file mode 100644 index 000000000..d7c198394 --- /dev/null +++ b/client/homebrew/utils/request-middleware.spec.js @@ -0,0 +1,74 @@ +import requestMiddleware from './request-middleware'; + +jest.mock('superagent'); +import request from 'superagent'; + +describe('request-middleware', ()=>{ + let version; + + let setFn; + let testFn; + + beforeEach(()=>{ + jest.resetAllMocks(); + version = global.version; + + global.version = '999'; + + setFn = jest.fn(); + testFn = jest.fn(()=>{ return { set: setFn }; }); + }); + + afterEach(()=>{ + global.version = version; + }); + + it('should add header to get', ()=>{ + // Ensure tests functions have been reset + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.get = testFn; + + requestMiddleware.get('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); + + it('should add header to put', ()=>{ + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.put = testFn; + + requestMiddleware.put('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); + + it('should add header to post', ()=>{ + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.post = testFn; + + requestMiddleware.post('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); + + it('should add header to delete', ()=>{ + expect(testFn).not.toHaveBeenCalled(); + expect(setFn).not.toHaveBeenCalled(); + + request.delete = testFn; + + requestMiddleware.delete('path'); + + expect(testFn).toHaveBeenCalledWith('path'); + expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999'); + }); +}); diff --git a/package-lock.json b/package-lock.json index eb82b0f40..fb815cdc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,19 +63,19 @@ "written-number": "^0.11.1" }, "devDependencies": { - "@stylistic/stylelint-plugin": "^3.1.3", + "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.31.0", + "eslint": "^9.35.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", - "jest": "^30.0.5", + "jest": "^30.1.3", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.22.0", - "stylelint-config-recess-order": "^7.1.0", - "stylelint-config-recommended": "^16.0.0", + "stylelint": "^16.24.0", + "stylelint-config-recess-order": "^7.3.0", + "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" }, "engines": { @@ -1824,9 +1824,10 @@ } }, "node_modules/@babel/types": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", - "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -1884,9 +1885,9 @@ } }, "node_modules/@csstools/media-query-list-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", - "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", "dev": true, "funding": [ { @@ -1898,12 +1899,13 @@ "url": "https://opencollective.com/csstools" } ], + "license": "MIT", "engines": { "node": ">=18" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.1", - "@csstools/css-tokenizer": "^3.0.1" + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, "node_modules/@dmsnell/diff-match-patch": { @@ -1924,14 +1926,14 @@ } }, "node_modules/@emnapi/core": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", - "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.0.4", + "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, @@ -1944,9 +1946,9 @@ "optional": true }, "node_modules/@emnapi/runtime": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", - "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", "dev": true, "license": "MIT", "optional": true, @@ -1963,9 +1965,9 @@ "optional": true }, "node_modules/@emnapi/wasi-threads": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", - "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", "dev": true, "license": "MIT", "optional": true, @@ -1982,17 +1984,20 @@ "optional": true }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", + "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -2021,18 +2026,19 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", - "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2080,9 +2086,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", - "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -2102,13 +2108,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", - "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.14.0", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -2208,9 +2214,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { @@ -2410,16 +2416,16 @@ } }, "node_modules/@jest/console": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", - "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.2.tgz", + "integrity": "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.0.5", + "jest-message-util": "30.1.0", "jest-util": "30.0.5", "slash": "^3.0.0" }, @@ -2428,17 +2434,17 @@ } }, "node_modules/@jest/core": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", - "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.3.tgz", + "integrity": "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.0.5", + "@jest/console": "30.1.2", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/reporters": "30.1.3", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", @@ -2447,18 +2453,18 @@ "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-changed-files": "30.0.5", - "jest-config": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", + "jest-config": "30.1.3", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-resolve-dependencies": "30.0.5", - "jest-runner": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", + "jest-resolve": "30.1.3", + "jest-resolve-dependencies": "30.1.3", + "jest-runner": "30.1.3", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", "jest-util": "30.0.5", - "jest-validate": "30.0.5", - "jest-watcher": "30.0.5", + "jest-validate": "30.1.0", + "jest-watcher": "30.1.3", "micromatch": "^4.0.8", "pretty-format": "30.0.5", "slash": "^3.0.0" @@ -2486,13 +2492,13 @@ } }, "node_modules/@jest/environment": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", - "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.2.tgz", + "integrity": "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.0.5", + "@jest/fake-timers": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "jest-mock": "30.0.5" @@ -2502,43 +2508,43 @@ } }, "node_modules/@jest/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.0.5", - "jest-snapshot": "30.0.5" + "expect": "30.1.2", + "jest-snapshot": "30.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", - "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1" + "@jest/get-type": "30.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", - "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.2.tgz", + "integrity": "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==", "dev": true, "license": "MIT", "dependencies": { "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.0.5", + "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-util": "30.0.5" }, @@ -2547,9 +2553,9 @@ } }, "node_modules/@jest/get-type": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", - "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", "engines": { @@ -2557,14 +2563,14 @@ } }, "node_modules/@jest/globals": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", - "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.2.tgz", + "integrity": "sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", "@jest/types": "30.0.5", "jest-mock": "30.0.5" }, @@ -2587,16 +2593,16 @@ } }, "node_modules/@jest/reporters": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", - "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.3.tgz", + "integrity": "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/console": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", @@ -2610,9 +2616,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.0.5", + "jest-message-util": "30.1.0", "jest-util": "30.0.5", - "jest-worker": "30.0.5", + "jest-worker": "30.1.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -2690,9 +2696,9 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", - "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.2.tgz", + "integrity": "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==", "dev": true, "license": "MIT", "dependencies": { @@ -2721,13 +2727,13 @@ } }, "node_modules/@jest/test-result": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", - "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.3.tgz", + "integrity": "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.0.5", + "@jest/console": "30.1.2", "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" @@ -2737,15 +2743,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", - "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.3.tgz", + "integrity": "sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.0.5", + "@jest/test-result": "30.1.3", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", + "jest-haste-map": "30.1.0", "slash": "^3.0.0" }, "engines": { @@ -2753,9 +2759,9 @@ } }, "node_modules/@jest/transform": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", - "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", + "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2767,7 +2773,7 @@ "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", + "jest-haste-map": "30.1.0", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", "micromatch": "^4.0.8", @@ -2832,37 +2838,11 @@ } }, "node_modules/@keyv/serialize": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", - "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.0.tgz", + "integrity": "sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==", "dev": true, - "dependencies": { - "buffer": "^6.0.3" - } - }, - "node_modules/@keyv/serialize/node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } + "license": "MIT" }, "node_modules/@mongodb-js/saslprep": { "version": "1.3.0", @@ -2978,9 +2958,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.34.38", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", - "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", "dev": true, "license": "MIT" }, @@ -3005,17 +2985,17 @@ } }, "node_modules/@stylistic/stylelint-plugin": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.3.tgz", - "integrity": "sha512-85fsmzgsIVmyG3/GFrjuYj6Cz8rAM7IZiPiXCMiSMfoDOC1lOrzrXPDk24WqviAghnPqGpx8b0caK2PuewWGFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-4.0.0.tgz", + "integrity": "sha512-CFwt3K4Y/7bygNCLCQ8Sy4Hzgbhxq3BsNW0FIuYxl17HD3ywptm54ocyeiLVRrk5jtz1Zwks7Xr9eiZt8SWHAw==", "dev": true, + "license": "MIT", "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.1", - "@csstools/css-tokenizer": "^3.0.1", - "@csstools/media-query-list-parser": "^3.0.1", - "is-plain-object": "^5.0.0", - "postcss": "^8.4.41", - "postcss-selector-parser": "^6.1.2", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "postcss": "^8.5.6", + "postcss-selector-parser": "^7.1.0", "postcss-value-parser": "^4.2.0", "style-search": "^0.1.0" }, @@ -3023,7 +3003,7 @@ "node": "^18.12 || >=20.9" }, "peerDependencies": { - "stylelint": "^16.8.0" + "stylelint": "^16.22.0" } }, "node_modules/@tybys/wasm-util": { @@ -3081,13 +3061,13 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/estree": { @@ -3131,13 +3111,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", - "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.8.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/stack-utils": { @@ -4065,13 +4045,13 @@ } }, "node_modules/babel-jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", - "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", + "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.0.5", + "@jest/transform": "30.1.2", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.0", "babel-preset-jest": "30.0.1", @@ -4175,9 +4155,9 @@ "license": "0BSD" }, "node_modules/babel-preset-current-node-syntax": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", - "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "dev": true, "license": "MIT", "dependencies": { @@ -4198,7 +4178,7 @@ "@babel/plugin-syntax-top-level-await": "^7.14.5" }, "peerDependencies": { - "@babel/core": "^7.0.0" + "@babel/core": "^7.0.0 || ^8.0.0-0" } }, "node_modules/babel-preset-jest": { @@ -4675,22 +4655,24 @@ } }, "node_modules/cacheable": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.1.tgz", - "integrity": "sha512-Fa2BZY0CS9F0PFc/6aVA6tgpOdw+hmv9dkZOlHXII5v5Hw+meJBIWDcPrG9q/dXxGcNbym5t77fzmawrBQfTmQ==", + "version": "1.10.4", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.10.4.tgz", + "integrity": "sha512-Gd7ccIUkZ9TE2odLQVS+PDjIvQCdJKUlLdJRVvZu0aipj07Qfx+XIej7hhDrKGGoIxV5m5fT/kOJNJPQhQneRg==", "dev": true, + "license": "MIT", "dependencies": { - "hookified": "^1.10.0", - "keyv": "^5.3.4" + "hookified": "^1.11.0", + "keyv": "^5.5.0" } }, "node_modules/cacheable/node_modules/keyv": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.4.tgz", - "integrity": "sha512-ypEvQvInNpUe+u+w8BIcPkQvEqXquyyibWE/1NB5T2BTzIpS5cGEV1LZskDzPSTvNAaT4+5FutvzlvnkxOSKlw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.0.tgz", + "integrity": "sha512-QG7qR2tijh1ftOvClut4YKKg1iW6cx3GZsKoGyJPxHkGWK9oJhG9P3j5deP0QQOGDowBMVQFaP+Vm4NpGYvmIQ==", "dev": true, + "license": "MIT", "dependencies": { - "@keyv/serialize": "^1.0.3" + "@keyv/serialize": "^1.1.0" } }, "node_modules/cached-path-relative": { @@ -4885,13 +4867,16 @@ } }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, "node_modules/cjs-module-lexer": { @@ -6080,20 +6065,20 @@ } }, "node_modules/eslint": { - "version": "9.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", - "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", + "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.31.0", - "@eslint/plugin-kit": "^0.3.1", + "@eslint/js": "9.35.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -6258,19 +6243,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6479,16 +6451,16 @@ "license": "MIT" }, "node_modules/expect": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", - "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-util": "30.0.5" }, @@ -7648,10 +7620,11 @@ } }, "node_modules/hookified": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.10.0.tgz", - "integrity": "sha512-dJw0492Iddsj56U1JsSTm9E/0B/29a1AuoSLRAte8vQg/kaTGF3IgjEWT8c8yG4cC10+HisE1x5QAwR0Xwc+DA==", - "dev": true + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.12.0.tgz", + "integrity": "sha512-hMr1Y9TCLshScrBbV2QxJ9BROddxZ12MX9KsCtuGGy/3SmmN5H1PllKerrVlSotur9dlE8hmUKAOSa3WDzsZmQ==", + "dev": true, + "license": "MIT" }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", @@ -8590,9 +8563,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -8638,16 +8611,16 @@ } }, "node_modules/jest": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", - "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", + "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.0.5", + "@jest/core": "30.1.3", "@jest/types": "30.0.5", "import-local": "^3.2.0", - "jest-cli": "30.0.5" + "jest-cli": "30.1.3" }, "bin": { "jest": "bin/jest.js" @@ -8680,26 +8653,26 @@ } }, "node_modules/jest-circus": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", - "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", + "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/expect": "30.0.5", - "@jest/test-result": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", - "jest-runtime": "30.0.5", - "jest-snapshot": "30.0.5", + "jest-each": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", "jest-util": "30.0.5", "p-limit": "^3.1.0", "pretty-format": "30.0.5", @@ -8712,21 +8685,21 @@ } }, "node_modules/jest-cli": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", - "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.3.tgz", + "integrity": "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.0.5", - "@jest/test-result": "30.0.5", + "@jest/core": "30.1.3", + "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.0.5", + "jest-config": "30.1.3", "jest-util": "30.0.5", - "jest-validate": "30.0.5", + "jest-validate": "30.1.0", "yargs": "^17.7.2" }, "bin": { @@ -8745,31 +8718,31 @@ } }, "node_modules/jest-config": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", - "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", + "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.0.5", + "@jest/test-sequencer": "30.1.3", "@jest/types": "30.0.5", - "babel-jest": "30.0.5", + "babel-jest": "30.1.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.0.5", + "jest-circus": "30.1.3", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", + "jest-environment-node": "30.1.2", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-runner": "30.0.5", + "jest-resolve": "30.1.3", + "jest-runner": "30.1.3", "jest-util": "30.0.5", - "jest-validate": "30.0.5", + "jest-validate": "30.1.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", "pretty-format": "30.0.5", @@ -8844,14 +8817,14 @@ } }, "node_modules/jest-diff": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", - "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", "pretty-format": "30.0.5" }, @@ -8873,13 +8846,13 @@ } }, "node_modules/jest-each": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", - "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", + "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "@jest/types": "30.0.5", "chalk": "^4.1.2", "jest-util": "30.0.5", @@ -8890,19 +8863,19 @@ } }, "node_modules/jest-environment-node": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", - "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", + "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "jest-mock": "30.0.5", "jest-util": "30.0.5", - "jest-validate": "30.0.5" + "jest-validate": "30.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -8916,9 +8889,9 @@ "license": "MIT" }, "node_modules/jest-haste-map": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", - "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", "dev": true, "license": "MIT", "dependencies": { @@ -8929,7 +8902,7 @@ "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", "jest-util": "30.0.5", - "jest-worker": "30.0.5", + "jest-worker": "30.1.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -8941,13 +8914,13 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", - "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", + "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "pretty-format": "30.0.5" }, "engines": { @@ -8955,15 +8928,15 @@ } }, "node_modules/jest-matcher-utils": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", - "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.0.5", + "jest-diff": "30.1.2", "pretty-format": "30.0.5" }, "engines": { @@ -8971,9 +8944,9 @@ } }, "node_modules/jest-message-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", - "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", "dev": true, "license": "MIT", "dependencies": { @@ -9035,18 +9008,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", - "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", + "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", + "jest-haste-map": "30.1.0", "jest-pnp-resolver": "^1.2.3", "jest-util": "30.0.5", - "jest-validate": "30.0.5", + "jest-validate": "30.1.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -9055,30 +9028,30 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", - "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.3.tgz", + "integrity": "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.0.5" + "jest-snapshot": "30.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", - "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", + "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.0.5", - "@jest/environment": "30.0.5", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/console": "30.1.2", + "@jest/environment": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", @@ -9086,15 +9059,15 @@ "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.0.1", - "jest-environment-node": "30.0.5", - "jest-haste-map": "30.0.5", - "jest-leak-detector": "30.0.5", - "jest-message-util": "30.0.5", - "jest-resolve": "30.0.5", - "jest-runtime": "30.0.5", + "jest-environment-node": "30.1.2", + "jest-haste-map": "30.1.0", + "jest-leak-detector": "30.1.0", + "jest-message-util": "30.1.0", + "jest-resolve": "30.1.3", + "jest-runtime": "30.1.3", "jest-util": "30.0.5", - "jest-watcher": "30.0.5", - "jest-worker": "30.0.5", + "jest-watcher": "30.1.3", + "jest-worker": "30.1.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -9103,18 +9076,18 @@ } }, "node_modules/jest-runtime": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", - "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", + "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.0.5", - "@jest/fake-timers": "30.0.5", - "@jest/globals": "30.0.5", + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/globals": "30.1.2", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", @@ -9122,12 +9095,12 @@ "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.0.5", - "jest-message-util": "30.0.5", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", "jest-mock": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.0.5", - "jest-snapshot": "30.0.5", + "jest-resolve": "30.1.3", + "jest-snapshot": "30.1.2", "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" @@ -9184,9 +9157,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", - "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", + "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -9195,18 +9168,18 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.0.5", - "@jest/get-type": "30.0.1", - "@jest/snapshot-utils": "30.0.5", - "@jest/transform": "30.0.5", + "@jest/expect-utils": "30.1.2", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.1.2", + "@jest/transform": "30.1.2", "@jest/types": "30.0.5", "babel-preset-current-node-syntax": "^1.1.0", "chalk": "^4.1.2", - "expect": "30.0.5", + "expect": "30.1.2", "graceful-fs": "^4.2.11", - "jest-diff": "30.0.5", - "jest-matcher-utils": "30.0.5", - "jest-message-util": "30.0.5", + "jest-diff": "30.1.2", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", "jest-util": "30.0.5", "pretty-format": "30.0.5", "semver": "^7.7.2", @@ -9261,13 +9234,13 @@ } }, "node_modules/jest-validate": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", - "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", + "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.0.1", + "@jest/get-type": "30.1.0", "@jest/types": "30.0.5", "camelcase": "^6.3.0", "chalk": "^4.1.2", @@ -9292,13 +9265,13 @@ } }, "node_modules/jest-watcher": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", - "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", + "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.0.5", + "@jest/test-result": "30.1.3", "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", @@ -9312,9 +9285,9 @@ } }, "node_modules/jest-worker": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", - "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", "dev": true, "license": "MIT", "dependencies": { @@ -10635,9 +10608,9 @@ } }, "node_modules/napi-postinstall": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", - "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", "dev": true, "license": "MIT", "bin": { @@ -11573,10 +11546,11 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -12560,16 +12534,23 @@ "license": "ISC" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shasum": { @@ -13306,9 +13287,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.22.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.22.0.tgz", - "integrity": "sha512-SVEMTdjKNV4ollUrIY9ordZ36zHv2/PHzPjfPMau370MlL2VYXeLgSNMMiEbLGRO8RmD2R8/BVUeF2DfnfkC0w==", + "version": "16.24.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.24.0.tgz", + "integrity": "sha512-7ksgz3zJaSbTUGr/ujMXvLVKdDhLbGl3R/3arNudH7z88+XZZGNLMTepsY28WlnvEFcuOmUe7fg40Q3lfhOfSQ==", "dev": true, "funding": [ { @@ -13335,7 +13316,7 @@ "debug": "^4.4.1", "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^10.1.1", + "file-entry-cache": "^10.1.4", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", @@ -13369,19 +13350,20 @@ } }, "node_modules/stylelint-config-recess-order": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.1.0.tgz", - "integrity": "sha512-rFc4Z6SCGgEohr1khsmAZ83X56Tdi2dHY/GB7VT3qJkpKU1V2w+mYlK+b7Za5gpsxEng3jnb4FzWyIl/KTH0AQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-7.3.0.tgz", + "integrity": "sha512-1LZhQi/D6OljSLRKejMEzbZA8h0AKkJH7p2y+eValc9ltWRGVznjnZsNLVCOwYpKk7GlYMLNVYTc9WpA0W3TYQ==", "dev": true, + "license": "ISC", "peerDependencies": { "stylelint": ">=16.18", "stylelint-order": ">=7" } }, "node_modules/stylelint-config-recommended": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-16.0.0.tgz", - "integrity": "sha512-4RSmPjQegF34wNcK1e1O3Uz91HN8P1aFdFzio90wNK9mjgAI19u5vsU868cVZboKzCaa5XbpvtTzAAGQAxpcXA==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-17.0.0.tgz", + "integrity": "sha512-WaMSdEiPfZTSFVoYmJbxorJfA610O0tlYuU2aEwY33UQhSPgFbClrVJYWvy3jGJx+XW37O+LyNLiZOEXhKhJmA==", "dev": true, "funding": [ { @@ -13398,7 +13380,7 @@ "node": ">=18.12.0" }, "peerDependencies": { - "stylelint": "^16.16.0" + "stylelint": "^16.23.0" } }, "node_modules/stylelint-order": { @@ -13418,29 +13400,6 @@ "stylelint": "^16.18.0" } }, - "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", - "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, "node_modules/stylelint/node_modules/@csstools/selector-specificity": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", @@ -13472,23 +13431,25 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.1.tgz", - "integrity": "sha512-zcmsHjg2B2zjuBgjdnB+9q0+cWcgWfykIcsDkWDB4GTPtl1eXUA+gTI6sO0u01AqK3cliHryTU55/b2Ow1hfZg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz", + "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^6.1.10" + "flat-cache": "^6.1.13" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.11.tgz", - "integrity": "sha512-zfOAns94mp7bHG/vCn9Ru2eDCmIxVQ5dELUHKjHfDEOJmHNzE+uGa6208kfkgmtym4a0FFjEuFksCXFacbVhSg==", + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.13.tgz", + "integrity": "sha512-gmtS2PaUjSPa4zjObEIn4WWliKyZzYljgxODBfxugpK6q6HU9ClXzgCJ+nlcPKY9Bt090ypTOLIFWkV0jbKFjw==", "dev": true, + "license": "MIT", "dependencies": { - "cacheable": "^1.10.1", + "cacheable": "^1.10.4", "flatted": "^3.3.3", - "hookified": "^1.10.0" + "hookified": "^1.11.0" } }, "node_modules/stylelint/node_modules/ignore": { @@ -13500,20 +13461,6 @@ "node": ">= 4" } }, - "node_modules/stylelint/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/stylelint/node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -14215,9 +14162,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index e32f5a05c..b36b9959a 100644 --- a/package.json +++ b/package.json @@ -136,19 +136,19 @@ "written-number": "^0.11.1" }, "devDependencies": { - "@stylistic/stylelint-plugin": "^3.1.3", + "@stylistic/stylelint-plugin": "^4.0.0", "babel-plugin-transform-import-meta": "^2.3.3", - "eslint": "^9.31.0", + "eslint": "^9.35.0", "eslint-plugin-jest": "^29.0.1", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", - "jest": "^30.0.5", + "jest": "^30.1.3", "jest-expect-message": "^1.1.3", "jsdom-global": "^3.0.2", "postcss-less": "^6.0.0", - "stylelint": "^16.22.0", - "stylelint-config-recess-order": "^7.1.0", - "stylelint-config-recommended": "^16.0.0", + "stylelint": "^16.24.0", + "stylelint-config-recess-order": "^7.3.0", + "stylelint-config-recommended": "^17.0.0", "supertest": "^7.1.4" } } diff --git a/server/app.js b/server/app.js index 869fe6555..afba0997b 100644 --- a/server/app.js +++ b/server/app.js @@ -487,8 +487,8 @@ app.get('/account', asyncHandler(async (req, res, next)=>{ const query = { authors: req.account.username, googleId: { $exists: false } }; const mongoCount = await HomebrewModel.countDocuments(query) .catch((err)=>{ - mongoCount = 0; console.log(err); + return 0; }); data.accountDetails = { diff --git a/shared/helpers.js b/shared/helpers.js index e09b0bdc4..3f91583d6 100644 --- a/shared/helpers.js +++ b/shared/helpers.js @@ -116,27 +116,21 @@ const printCurrentBrew = ()=>{ } }; -const fetchThemeBundle = async (obj, renderer, theme)=>{ +const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{ if(!renderer || !theme) return; const res = await request .get(`/api/theme/${renderer}/${theme}`) .catch((err)=>{ - obj.setState({ error: err }); + setError(err) }); if(!res) { - obj.setState((prevState)=>({ - ...prevState, - themeBundle : {} - })); + setThemeBundle({}); return; } const themeBundle = res.body; themeBundle.joinedStyles = themeBundle.styles.map((style)=>``).join('\n\n'); - obj.setState((prevState)=>({ - ...prevState, - themeBundle : themeBundle, - error : null - })); + setThemeBundle(themeBundle); + setError(null); }; const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => { diff --git a/themes/V3/UnearthedArcana/dropdownPreview.png b/themes/V3/UnearthedArcana/dropdownPreview.png new file mode 100644 index 000000000..cfc1e36bc Binary files /dev/null and b/themes/V3/UnearthedArcana/dropdownPreview.png differ diff --git a/themes/V3/UnearthedArcana/dropdownTexture.png b/themes/V3/UnearthedArcana/dropdownTexture.png new file mode 100644 index 000000000..d0c0256c0 Binary files /dev/null and b/themes/V3/UnearthedArcana/dropdownTexture.png differ diff --git a/themes/V3/UnearthedArcana/settings.json b/themes/V3/UnearthedArcana/settings.json new file mode 100644 index 000000000..273f0bb2f --- /dev/null +++ b/themes/V3/UnearthedArcana/settings.json @@ -0,0 +1,6 @@ +{ + "name" : "UnearthedArcana", + "renderer" : "V3", + "baseTheme" : false, + "baseSnippets" : false +} diff --git a/themes/V3/UnearthedArcana/style.less b/themes/V3/UnearthedArcana/style.less new file mode 100644 index 000000000..695924d37 --- /dev/null +++ b/themes/V3/UnearthedArcana/style.less @@ -0,0 +1,38 @@ +@import (less) './themes/fonts/5e/fonts.less'; +@import (less) './themes/assets/assets.less'; + +:root { + //Colors + --HB_Color_Background : #FFFFFF; // White + --HB_Color_WatercolorStain : #000000; // Black +} + +.page { + font-family: Cambria,Georgia,serif; + font-size: 14px; + h1, h2, h3, h4 { + font-variant: small-caps; + font-weight: normal; + } + h1 { + column-span: all; + -webkit-column-span: all; + font-size: 40px; + } + h2 { + font-size: 26px; + } + h3 { + font-size: 20px; + border-bottom: 2px solid black; + } + h4 { + font-size: 18px; + } + h5 { + font-size: 16px; + } + h6 { + font-size: 14px; + } +} diff --git a/themes/themes.json b/themes/themes.json index 16a4b9b13..7e01b180c 100644 --- a/themes/themes.json +++ b/themes/themes.json @@ -35,6 +35,13 @@ "baseTheme": "Blank", "baseSnippets": "5ePHB", "path": "Journal" + }, + "UnearthedArcana": { + "name": "UnearthedArcana", + "renderer": "V3", + "baseTheme": false, + "baseSnippets": false, + "path": "UnearthedArcana" } } } \ No newline at end of file