diff --git a/.circleci/config.yml b/.circleci/config.yml index 274ec25ac..f18f84943 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ orbs: jobs: build: docker: - - image: cimg/node:20.8.0 + - image: cimg/node:20.17.0 - image: mongo:4.4 working_directory: ~/homebrewery @@ -27,7 +27,7 @@ jobs: # fallback to using the latest cache if no exact match is found - v1-dependencies- - - run: sudo npm install -g npm@10.2.0 + - run: sudo npm install -g npm@10.8.2 - node/install-packages: app-dir: ~/homebrewery cache-path: node_modules @@ -45,7 +45,7 @@ jobs: test: docker: - - image: cimg/node:20.8.0 + - image: cimg/node:20.17.0 working_directory: ~/homebrewery parallelism: 1 diff --git a/changelog.md b/changelog.md index 9d1ddf32d..1f7815d8d 100644 --- a/changelog.md +++ b/changelog.md @@ -81,9 +81,85 @@ pre { } ``` + ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). +### Saturday 10/12/2024 - v3.16.0 + +{{taskList +##### 5e-Cleric + +* [x] Added a new API endpoint `/metadata/:shareId` to fetch metadata about individual brews + +Fixes issue [#2638](https://github.com/naturalcrit/homebrewery/issues/2638) + +* [x] Added A3, A5, and Card page size snippets under {{openSans **:fas_paintbrush: STYLE TAB :fas_arrow_right: :fas_print: PRINT**}} + +* [x] Adjust navbar styling for very long titles + +Fixes issue [#2071](https://github.com/naturalcrit/homebrewery/issues/2071) + +* [x] Added some sorting options to the {{openSans **VAULT** {{fas,fa-dungeon}}}} page + +* [x] Fix `language` property not working in share page + +Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776) + +##### abquintic + +* [x] New {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBER :fas_arrow_right:**}} +{{openSans **:fas_xmark: SKIP PAGE NUMBER**}} and {{openSans **:fas_arrow_rotate_left: RESTART PAGE NUMBER**}} snippets for more control over automatic page numbering. + +Fixes issue [#513](https://github.com/naturalcrit/homebrewery/issues/513) + +* [x] New Table of Contents control options via {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_book: TABLE OF CONTENTS**}} submenus. By default, H1-H3 is included in the ToC generation, but the new options allow marking `{{blocks}}` to include or exclude specific or ranges of contained headers. Also, a global option to increase the default range of H1-H3 to H1-H4/5/6. After applying these markers, you must regenerate the Table of Contents to see the changes. + +* [x] Added a ":fas_lock: SYNC VIEWS" button onto the divider bar. When locked, scrolling on either panel will sync the other panel to the same page. + +Fixes issue [#241](https://github.com/naturalcrit/homebrewery/issues/241) + +##### Gazook89 + +* [x] Added a :fas_glasses: HIDE button to the page navigation bar + +##### G-Ambatte + +* [x] Automatic local backups of your files, in case of accidental data loss. Stores up to 5 snapshots of each brew edited in your browser, incrementing from a few minutes old to a maximum of several days. Restore a backup by clicking an entry in the new {{openSans **:fas_clock_rotate_left: HISTORY**}} button in the snippet bar. + +Fixes issue [#3070](https://github.com/naturalcrit/homebrewery/issues/3070) + +* [x] Fix issue with legacy brews breaking on Share page + +Fixes issue [#3764](https://github.com/naturalcrit/homebrewery/issues/3764) + +* [x] Fix print size when printing a zoomed document + +Fixes issue [#3744](https://github.com/naturalcrit/homebrewery/issues/3744) + +##### All + +* [x] Background code cleanup, security fixes, dev tool improvements, dependency updates, prep for upcoming features, etc. +}} + +### Wednesday 9/25/2024 - v3.15.1 + +{{taskList +##### calculuschild + +* [x] Background fixes to handle Google Drive issues + +* [x] Remove duplicate error logging + +##### calculuschild, 5e-Cleric + +* [x] Fix links in {{openSans **RECENT BREWS :fas_clock_rotate_left:**}} and user {{openSans **BREWS :fas_beer_mug_empty:**}} pointing to trashed Google Drive files after transferring from Google to Homebrewery storage + +Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776) +}} + +\page + ### Wednesday 9/04/2024 - v3.15.0 {{taskList diff --git a/client/admin/admin.jsx b/client/admin/admin.jsx index 92e0b2aee..f2f2667a4 100644 --- a/client/admin/admin.jsx +++ b/client/admin/admin.jsx @@ -2,35 +2,44 @@ require('./admin.less'); const React = require('react'); const createClass = require('create-react-class'); +const BrewUtils = require('./brewUtils/brewUtils.jsx'); +const NotificationUtils = require('./notificationUtils/notificationUtils.jsx'); -const BrewCleanup = require('./brewCleanup/brewCleanup.jsx'); -const BrewLookup = require('./brewLookup/brewLookup.jsx'); -const BrewCompress = require ('./brewCompress/brewCompress.jsx'); -const Stats = require('./stats/stats.jsx'); +const tabGroups = ['brew', 'notifications']; const Admin = createClass({ getDefaultProps : function() { return {}; }, + getInitialState : function(){ + return ({ + currentTab : 'brew' + }); + }, + + handleClick : function(newTab){ + if(this.state.currentTab === newTab) return; + this.setState({ + currentTab : newTab + }); + }, + render : function(){ return
-
homebrewery admin
-
- -
- -
- -
- -
+
+ + {this.state.currentTab==='brew' && } + {this.state.currentTab==='notifications' && } +
; } }); diff --git a/client/admin/admin.less b/client/admin/admin.less index a61335835..c6c9b4662 100644 --- a/client/admin/admin.less +++ b/client/admin/admin.less @@ -6,39 +6,95 @@ @import 'font-awesome/css/font-awesome.css'; -html,body, #reactContainer, .naturalCrit{ - min-height : 100%; -} +html,body, #reactContainer, .naturalCrit { min-height : 100%; } @sidebarWidth : 250px; -body{ - background-color : #eee; - font-family : 'Open Sans', sans-serif; - color : #4b5055; - font-weight : 100; - text-rendering : optimizeLegibility; - margin : 0; +body { + height : 100%; padding : 0; - height : 100%; + margin : 0; + font-family : 'Open Sans', sans-serif; + font-weight : 100; + color : #4B5055; + background-color : #EEEEEE; + text-rendering : optimizeLegibility; } -.admin{ +:where(.admin) { - header{ + header { + padding : 20px 0px; + margin-bottom : 30px; + font-size : 2em; + color : white; background-color : @red; - font-size: 2em; - padding : 20px 0px; - color : white; - margin-bottom: 30px; - i{ - margin-right: 30px; + i { margin-right : 30px; } + } + + hr { margin : 30px 0px; } + + :where(.container) { + input { + height : 33px; + padding : 0px 10px; + margin-bottom : 20px; + font-family : monospace; + } + + button { + height : 37px; + vertical-align : middle; + } + + dl { + @maxItemWidth : 132px; + dt { + float : left; + width : @maxItemWidth; + clear : left; + text-align : right; + &::after { content : ' : '; } + } + dd { + height : 1em; + padding : 0 0 0.5em 0; + margin-left : @maxItemWidth + 6px; + } + } + + .tabs button { + margin-right : 3px; + margin-left : 3px; + color : black; + background-color : #EEEEEE; + border : 1px solid #444444; + border-radius : 5px; + &:hover { + color : #EEEEEE; + background-color : #444444; + } + &.active { + margin-right : 2px; + margin-left : 2px; + text-decoration : underline; + background-color : #CCCCCC; + border : 2px solid #444444; + } + } + + .notificationUtils { + display : flex; + gap : 50px; + justify-content : space-between; } } - hr{ - margin : 30px 0px; + .error { + background: rgb(178, 54, 54); + color:white; + font-weight: 900; + margin-block:10px; + padding:10px; } - - } diff --git a/client/admin/brewCleanup/brewCleanup.less b/client/admin/brewCleanup/brewCleanup.less deleted file mode 100644 index ec7582855..000000000 --- a/client/admin/brewCleanup/brewCleanup.less +++ /dev/null @@ -1,10 +0,0 @@ -.BrewCleanup{ - .removeBox{ - margin-top: 20px; - button{ - background-color: @red; - margin-right: 10px; - } - } - -} \ No newline at end of file diff --git a/client/admin/brewCompress/brewCompress.less b/client/admin/brewCompress/brewCompress.less deleted file mode 100644 index 2a2bf42ea..000000000 --- a/client/admin/brewCompress/brewCompress.less +++ /dev/null @@ -1,10 +0,0 @@ -.BrewCompress{ - .removeBox{ - margin-top: 20px; - button{ - background-color: @red; - margin-right: 10px; - } - } - -} \ No newline at end of file diff --git a/client/admin/brewLookup/brewLookup.less b/client/admin/brewLookup/brewLookup.less deleted file mode 100644 index 61eeec424..000000000 --- a/client/admin/brewLookup/brewLookup.less +++ /dev/null @@ -1,30 +0,0 @@ - -.brewLookup{ - input{ - height : 33px; - margin-bottom : 20px; - padding : 0px 10px; - font-family : monospace; - } - button{ - vertical-align : middle; - height : 37px; - } - dl{ - @maxItemWidth : 132px; - dt{ - float : left; - clear : left; - width : @maxItemWidth; - text-align : right; - &::after { - content: " : "; - } - } - dd{ - height : 1em; - margin-left : @maxItemWidth + 6px; - padding : 0 0 0.5em 0; - } - } -} \ No newline at end of file diff --git a/client/admin/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx similarity index 100% rename from client/admin/brewCleanup/brewCleanup.jsx rename to client/admin/brewUtils/brewCleanup/brewCleanup.jsx diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.less b/client/admin/brewUtils/brewCleanup/brewCleanup.less new file mode 100644 index 000000000..16fc98957 --- /dev/null +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.less @@ -0,0 +1,9 @@ +.BrewCleanup { + .removeBox { + margin-top : 20px; + button { + margin-right : 10px; + background-color : @red; + } + } +} \ No newline at end of file diff --git a/client/admin/brewCompress/brewCompress.jsx b/client/admin/brewUtils/brewCompress/brewCompress.jsx similarity index 100% rename from client/admin/brewCompress/brewCompress.jsx rename to client/admin/brewUtils/brewCompress/brewCompress.jsx diff --git a/client/admin/brewUtils/brewCompress/brewCompress.less b/client/admin/brewUtils/brewCompress/brewCompress.less new file mode 100644 index 000000000..8668e9280 --- /dev/null +++ b/client/admin/brewUtils/brewCompress/brewCompress.less @@ -0,0 +1,9 @@ +.BrewCompress { + .removeBox { + margin-top : 20px; + button { + margin-right : 10px; + background-color : @red; + } + } +} \ No newline at end of file diff --git a/client/admin/brewLookup/brewLookup.jsx b/client/admin/brewUtils/brewLookup/brewLookup.jsx similarity index 98% rename from client/admin/brewLookup/brewLookup.jsx rename to client/admin/brewUtils/brewLookup/brewLookup.jsx index c9212d990..50a2f2015 100644 --- a/client/admin/brewLookup/brewLookup.jsx +++ b/client/admin/brewUtils/brewLookup/brewLookup.jsx @@ -1,4 +1,3 @@ -require('./brewLookup.less'); const React = require('react'); const createClass = require('create-react-class'); const cx = require('classnames'); diff --git a/client/admin/brewUtils/brewUtils.jsx b/client/admin/brewUtils/brewUtils.jsx new file mode 100644 index 000000000..de8c29895 --- /dev/null +++ b/client/admin/brewUtils/brewUtils.jsx @@ -0,0 +1,24 @@ +const React = require('react'); +const createClass = require('create-react-class'); + + +const BrewCleanup = require('./brewCleanup/brewCleanup.jsx'); +const BrewLookup = require('./brewLookup/brewLookup.jsx'); +const BrewCompress = require ('./brewCompress/brewCompress.jsx'); +const Stats = require('./stats/stats.jsx'); + +const BrewUtils = createClass({ + render : function(){ + return <> + +
+ +
+ +
+ + ; + } +}); + +module.exports = BrewUtils; diff --git a/client/admin/stats/stats.jsx b/client/admin/brewUtils/stats/stats.jsx similarity index 100% rename from client/admin/stats/stats.jsx rename to client/admin/brewUtils/stats/stats.jsx diff --git a/client/admin/brewUtils/stats/stats.less b/client/admin/brewUtils/stats/stats.less new file mode 100644 index 000000000..b5a4612e1 --- /dev/null +++ b/client/admin/brewUtils/stats/stats.less @@ -0,0 +1,13 @@ + +.Stats { + position : relative; + + .pending { + position : absolute; + top : 0px; + left : 0px; + width : 100%; + height : 100%; + background-color : rgba(238,238,238, 0.5); + } +} \ No newline at end of file diff --git a/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx b/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx new file mode 100644 index 000000000..5a8ebf5d0 --- /dev/null +++ b/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx @@ -0,0 +1,109 @@ +require('./notificationAdd.less'); +const React = require('react'); +const { useState, useRef } = require('react'); +const request = require('superagent'); + +const NotificationAdd = ()=>{ + const [notificationResult, setNotificationResult] = useState(null); + const [searching, setSearching] = useState(false); + const [error, setError] = useState(null); + + const dismissKeyRef = useRef(null); + const titleRef = useRef(null); + const textRef = useRef(null); + const startAtRef = useRef(null); + const stopAtRef = useRef(null); + + const saveNotification = async ()=>{ + const dismissKey = dismissKeyRef.current.value; + const title = titleRef.current.value; + const text = textRef.current.value; + const startAt = new Date(startAtRef.current.value); + const stopAt = new Date(stopAtRef.current.value); + + // Basic validation + if(!dismissKey || !title || !text || isNaN(startAt.getTime()) || isNaN(stopAt.getTime())) { + setError('All fields are required'); + return; + } + if(startAt >= stopAt) { + setError('End date must be after the start date!'); + return; + } + + const data = { + dismissKey, + title, + text, + startAt : startAt?.toISOString() ?? '', + stopAt : stopAt?.toISOString() ?? '', + }; + + try { + setSearching(true); + setError(null); + const response = await request.post('/admin/notification/add').send(data); + console.log(response.body); + + // Reset form fields + dismissKeyRef.current.value = ''; + titleRef.current.value = ''; + textRef.current.value = ''; + + setNotificationResult('Notification successfully created.'); + setSearching(false); + } catch (err) { + console.log(err.response.body.message); + setError(`Error saving notification: ${err.response.body.message}`); + setSearching(false); + } + }; + + return ( +
+

Add Notification

+ + + + + + + + + + + +
{notificationResult}
+ + + {error &&
{error}
} +
+ ); +}; + +module.exports = NotificationAdd; diff --git a/client/admin/notificationUtils/notificationAdd/notificationAdd.less b/client/admin/notificationUtils/notificationAdd/notificationAdd.less new file mode 100644 index 000000000..878da24c2 --- /dev/null +++ b/client/admin/notificationUtils/notificationAdd/notificationAdd.less @@ -0,0 +1,37 @@ +.notificationAdd { + position : relative; + display : flex; + flex-direction : column; + width : 500px; + + .field { + display : grid; + grid-template-columns : 120px 150px; + align-items : center; + justify-items : stretch; + width : 100%; + margin-bottom : 20px; + + + input { + height : 33px; + padding : 0px 10px; + margin-bottom : unset; + font-family : monospace; + } + + textarea { + width : 50ch; + min-height : 7em; + max-height : 20em; + resize : vertical; + padding : 10px; + } + } + + button { + width: 200px; + + i { margin-right : 10px; } + } +} \ No newline at end of file diff --git a/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx b/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx new file mode 100644 index 000000000..71f8da59c --- /dev/null +++ b/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx @@ -0,0 +1,105 @@ +require('./notificationLookup.less'); + +const React = require('react'); +const { useState } = require('react'); +const request = require('superagent'); +const Moment = require('moment'); + +const NotificationDetail = ({ notification, onDelete })=>( + <> +
+
Key
+
{notification.dismissKey}
+ +
Title
+
{notification.title || 'No Title'}
+ +
Text
+
{notification.text || 'No Text'}
+ +
Created
+
{Moment(notification.createdAt).format('LLLL')}
+ +
Start
+
{Moment(notification.startAt).format('LLLL') || 'No Start Time'}
+ +
Stop
+
{Moment(notification.stopAt).format('LLLL') || 'No End Time'}
+
+ + +); + +const NotificationLookup = ()=>{ + const [searching, setSearching] = useState(false); + const [error, setError] = useState(null); + const [notifications, setNotifications] = useState([]); + + const lookupAll = async ()=>{ + setSearching(true); + setError(null); + + try { + const res = await request.get('/admin/notification/all'); + setNotifications(res.body || []); + } catch (err) { + console.log(err); + setError(`Error looking up notifications: ${err.response.body.message}`); + } finally { + setSearching(false); + } + }; + + const deleteNotification = async (dismissKey)=>{ + if(!dismissKey) return; + + const confirmed = window.confirm( + `Really delete notification ${dismissKey}?` + ); + if(!confirmed) { + console.log('Delete notification cancelled'); + return; + } + console.log('Delete notification confirm'); + try { + await request.delete(`/admin/notification/delete/${dismissKey}`); + lookupAll(); + } catch (err) { + console.log(err); + setError(`Error deleting notification: ${err.response.body.message}`); + }; + }; + + const renderNotificationsList = ()=>{ + if(error) + return
{error}
; + + if(notifications.length === 0) + return
No notifications available.
; + + return ( +
    + {notifications.map((notification)=>( +
  • +
    + {notification.title || 'No Title'} + +
    +
  • + ))} +
+ ); + }; + + return ( +
+

Check all Notifications

+ + {renderNotificationsList()} +
+ ); +}; + +module.exports = NotificationLookup; diff --git a/client/admin/notificationUtils/notificationLookup/notificationLookup.less b/client/admin/notificationUtils/notificationLookup/notificationLookup.less new file mode 100644 index 000000000..3f9b78310 --- /dev/null +++ b/client/admin/notificationUtils/notificationLookup/notificationLookup.less @@ -0,0 +1,40 @@ + +.notificationLookup { + width : 450px; + height : fit-content; + + .notificationList { + display : flex; + flex-direction : column; + max-height : 500px; + margin-block : 20px; + overflow : auto; + border : 1px solid; + border-radius : 5px; + + li { + padding : 10px; + background : #CCCCCC; + + &:nth-child(even) { background : #DDDDDD; } + &:first-child { + border-top-left-radius : 5px; + border-top-right-radius : 5px; + } + &:last-child { + border-bottom-right-radius : 5px; + border-bottom-left-radius : 5px; + } + + summary { + font-size : 20px; + font-weight : 900; + } + + dl dt{ + font-weight: 900; + } + } + } + .noNotification { margin-block : 20px; } +} \ No newline at end of file diff --git a/client/admin/notificationUtils/notificationUtils.jsx b/client/admin/notificationUtils/notificationUtils.jsx new file mode 100644 index 000000000..22ea21328 --- /dev/null +++ b/client/admin/notificationUtils/notificationUtils.jsx @@ -0,0 +1,15 @@ +const React = require('react'); + +const NotificationLookup = require('./notificationLookup/notificationLookup.jsx'); +const NotificationAdd = require('./notificationAdd/notificationAdd.jsx'); + +const NotificationUtils = ()=>{ + return ( +
+ + +
+ ); +}; + +module.exports = NotificationUtils; diff --git a/client/admin/stats/stats.less b/client/admin/stats/stats.less deleted file mode 100644 index 5337bf671..000000000 --- a/client/admin/stats/stats.less +++ /dev/null @@ -1,28 +0,0 @@ - -.Stats{ - position : relative; - .pending{ - position : absolute; - top : 0px; - left : 0px; - height : 100%; - width : 100%; - background-color : rgba(238,238,238, 0.5); - } - dl{ - @maxItemWidth : 132px; - dt{ - float : left; - clear : left; - width : @maxItemWidth; - text-align : right; - &::after { - content: " : "; - } - } - dd{ - margin : 0 0 0 @maxItemWidth + 10px; - padding : 0 0 0.5em 0; - } - } -} \ No newline at end of file diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 8d63c1e0c..de8d2a803 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -1,7 +1,7 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ require('./brewRenderer.less'); const React = require('react'); -const { useState, useRef, useEffect, useCallback } = React; +const { useState, useRef, useCallback } = React; const _ = require('lodash'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); @@ -54,16 +54,15 @@ const BrewRenderer = (props)=>{ theme : '5ePHB', lang : '', errors : [], - currentEditorCursorPageNum : 0, - currentEditorViewPageNum : 0, - currentBrewRendererPageNum : 0, + currentEditorCursorPageNum : 1, + currentEditorViewPageNum : 1, + currentBrewRendererPageNum : 1, themeBundle : {}, onPageChange : ()=>{}, ...props }; const [state, setState] = useState({ - height : PAGE_HEIGHT, isMounted : false, visibility : 'hidden', zoom : 100 @@ -206,7 +205,6 @@ const BrewRenderer = (props)=>{ } }; - const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside if(!window || !document) return; document.dispatchEvent(new MouseEvent('click')); @@ -220,6 +218,12 @@ const BrewRenderer = (props)=>{ })); }; + const styleObject = {}; + + if(global.config.deployment) { + styleObject.backgroundImage = `url("data:image/svg+xml;utf8,${global.config.deployment}")`; + } + return ( <> {/*render dummy page while iFrame is mounting.*/} @@ -245,11 +249,11 @@ const BrewRenderer = (props)=>{ contentDidMount={frameDidMount} onClick={()=>{emitClick();}} > -
+ style={ styleObject }> {/* Apply CSS from Style tab and render pages from Markdown tab */} {state.isMounted diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less index dca64c455..81e7e4f22 100644 --- a/client/homebrew/brewRenderer/brewRenderer.less +++ b/client/homebrew/brewRenderer/brewRenderer.less @@ -4,6 +4,10 @@ overflow-y : scroll; will-change : transform; padding-top : 30px; + height : 100vh; + &.deployment { + background-color: darkred; + } :where(.pages) { margin : 30px 0px; & > :where(.page) { @@ -39,6 +43,7 @@ overflow-y : unset; .pages { margin : 0px; + zoom: 100% !important; & > .page { box-shadow : unset; } } } diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx index ebeb2ca5f..f4e0b1c95 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx @@ -15,34 +15,6 @@ const NotificationPopup = ()=>{ This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:
    -
  • - Known issue with saving/creating Google Drive files
    - Dear users. The - issue with saving to Google Drive has resurfaced as of Oct 1, 2024 22:00 UTC. -



    - Earlier we submitted a bug report to Google and have all but confirmed the issue - lies on Google's end and the disruption has been affecting multiple other - organizations besides us. Unfortunately, it means reliable interaction with - Google remains out of our control until they can resolve their issue. -



    - Brews saved to Google Drive are not lost and can still be viewed, just not updated. - You can also access them via your Google Drive interface in the /Hombrewery folder. -



    - If you need to urgently edit documents, you can detatch them from your Google Drive - by transferring them to our Homebrewery storage. To do this, click the colored Google Drive - icon next to the save button when on an edit page; you can transfer them back later, - but this should allow you to edit while this issue is ongoing. -



    - If you are experiencing errors creating new documents, you can similarly change your - account settings to create new brews by default in the Homebrewery storage. Click - your username and then "account", then change the "default save location". -
  • -
  • Search brews with our new page!
    We have been working very hard in making this possible, now you can share your work and look at it in the new Vault page! diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index 0f1f6ad54..e66fa64e2 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -304,17 +304,14 @@ const MetadataEditor = createClass({ onChange={(e)=>this.handleRenderer('V3', e)} /> V3 - - - Click here to see the demo page for the old Legacy renderer! - + Click here to see the demo page for the old Legacy renderer! ; }, render : function(){ return
    -

    Brew

    +

    Properties Editor

    @@ -362,9 +359,7 @@ const MetadataEditor = createClass({ {this.renderRenderOptions()} -
    - -

    Authors

    +

    Authors

    {this.renderAuthors()} @@ -375,15 +370,13 @@ const MetadataEditor = createClass({ notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/> -
    - -

    Privacy

    +

    Privacy

    {this.renderPublish()} - Published homebrews will be publicly viewable and searchable (eventually...) + Published brews are searchable in the Vault and visible on your user page. Unpublished brews are not indexed in the Vault or visible on your user page, but can still be shared and indexed by search engines. You can unpublish a brew any time.
    diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less index 27ebd88c2..62ec6b37b 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.less +++ b/client/homebrew/editor/metadataEditor/metadataEditor.less @@ -1,5 +1,6 @@ @import 'naturalcrit/styles/colors.less'; + .metadataEditor { position : absolute; z-index : 5; @@ -9,12 +10,19 @@ padding : 25px; overflow-y : auto; background-color : #999999; + font-size : 13px; - .sectionHead { + h1 { + margin: 0 0 40px; + font-weight: bold; + text-transform: uppercase; + } + + h2 { margin : 20px 0; - font-weight : 1000; - - &:first-of-type { margin-top : 0; } + font-weight : bold; + border-bottom: 2px solid gray; + color: #555; } & > div { margin-bottom : 10px; } @@ -43,15 +51,21 @@ min-width : 200px; & > label { width : 80px; - font-size : 11px; font-weight : 800; line-height : 1.8em; text-transform : uppercase; + font-size: .9em; } & > .value { flex : 1 1 auto; width : 50px; &:invalid { background : #FFB9B9; } + small { + display : block; + font-size : 0.9em; + font-style : italic; + line-height : 1.4em; + } } input[type='text'], textarea { border : 1px solid gray; @@ -78,7 +92,6 @@ textarea.value { height : auto; font-family : 'Open Sans', sans-serif; - font-size : 0.8em; resize : none; } } @@ -87,12 +100,6 @@ z-index : 200; max-width : 150px; } - small { - display : inline-block; - font-size : 0.6em; - font-style : italic; - line-height : 1.4em; - } } @@ -113,18 +120,13 @@ display : inline-flex; align-items : center; margin-right : 15px; - font-size : 0.7em; + font-size : 0.9em; font-weight : 800; white-space : nowrap; vertical-align : middle; cursor : pointer; user-select : none; } - a { - display : inline-flex; - font-size : 0.7em; - font-weight : 800; - } input { margin : 3px; vertical-align : middle; @@ -149,12 +151,10 @@ } } .authors.field .value { - font-size : 0.8em; line-height : 1.5em; } .themes.field { - font-size : 13.33px; .navDropdownContainer { position : relative; z-index : 100; @@ -165,9 +165,9 @@ background-color : darkgray; } & > div:first-child { - padding : 6px 3px; + padding : 3px 3px; background-color : inherit; - border : 2px solid rgb(118,118,118); + border : 1px solid gray; i { float : right; } &:hover { color : white; @@ -240,6 +240,7 @@ } } } + .field .list { display : flex; flex : 1 0; @@ -258,15 +259,15 @@ color : white; text-align : center; cursor : pointer; - + i { position : relative; top : 50%; transform : translateY(-50%); } - + &:not(:last-child) { border-right : 1px solid black; } - + &:last-child { border-radius : 0 0.5em 0.5em 0; } } @@ -277,8 +278,7 @@ background-color : #DDDDDD; border-radius : 0.5em; - .icon { - #groupedIcon; } + .icon { #groupedIcon; } } .input-group { @@ -294,17 +294,30 @@ height : 100%; } - .invalid:focus { background-color : pink; } + .input-group { + height : ~'calc(.9em + 4px + .6em)'; - .icon { - #groupedIcon; - top : -0.54em; - right : 1px; - height : 97%; - font-size : 0.8em; + input { border-radius : 0.5em 0 0 0.5em; } - i { font-size : 1.125em; } + input:last-child { border-radius : 0.5em; } + + .value { + width : 7.5vw; + min-width : 75px; + height : 100%; + } + + .invalid:focus { background-color : pink; } + + .icon { + #groupedIcon; + top : -0.54em; + right : 1px; + height : 97%; + + i { font-size : 1.125em; } + } } } } -} +} \ No newline at end of file diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index e19889cc7..d457d92f2 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -1,11 +1,11 @@ -/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/ +/*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/ require('./snippetbar.less'); const React = require('react'); const createClass = require('create-react-class'); const _ = require('lodash'); const cx = require('classnames'); -import { getHistoryItems, historyExists } from '../../utils/versionHistory.js'; +import { loadHistory } from '../../utils/versionHistory.js'; //Import all themes const ThemeSnippets = {}; @@ -50,27 +50,47 @@ const Snippetbar = createClass({ renderer : this.props.renderer, themeSelector : false, snippets : [], - historyExists : false + showHistory : false, + historyExists : false, + historyItems : [] }; }, - componentDidMount : async function() { + componentDidMount : async function(prevState) { const snippets = this.compileSnippets(); this.setState({ snippets : snippets }); }, - componentDidUpdate : async function(prevProps) { + componentDidUpdate : async function(prevProps, prevState) { if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) { this.setState({ snippets : this.compileSnippets() }); }; - this.setState({ - historyExists : historyExists(this.props.brew) + // Update history list if it has changed + const checkHistoryItems = await loadHistory(this.props.brew); + + // If all items have the noData property, there is no saved data + const checkHistoryExists = !checkHistoryItems.every((historyItem)=>{ + return historyItem?.noData; }); + if(prevState.historyExists != checkHistoryExists){ + this.setState({ + historyExists : checkHistoryExists + }); + } + + // If any history items have changed, update the list + if(checkHistoryExists && checkHistoryItems.some((historyItem, index)=>{ + return index >= prevState.historyItems.length || !_.isEqual(historyItem, prevState.historyItems[index]); + })){ + this.setState({ + historyItems : checkHistoryItems + }); + } }, mergeCustomizer : function(oldValue, newValue, key) { @@ -148,12 +168,18 @@ const Snippetbar = createClass({ return this.props.updateBrew(item); }, + toggleHistoryMenu : function(){ + this.setState({ + showHistory : !this.state.showHistory + }); + }, + renderHistoryItems : function() { - const historyItems = getHistoryItems(this.props.brew); + if(!this.state.historyExists) return; return
    - {_.map(historyItems, (item, index)=>{ - if(!item.savedAt) return; + {_.map(this.state.historyItems, (item, index)=>{ + if(item.noData || !item.savedAt) return; const saveTime = new Date(item.savedAt); const diffMs = new Date() - saveTime; @@ -194,9 +220,10 @@ const Snippetbar = createClass({ } return
    -
    +
    - {this.state.historyExists && this.renderHistoryItems() } + { this.state.showHistory && this.renderHistoryItems() }
    diff --git a/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx b/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx index 8f06ae561..47ab038cc 100644 --- a/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx +++ b/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx @@ -128,7 +128,7 @@ const StringArrayEditor = createClass({ return
    -
    +
    {valueElements}
    diff --git a/client/homebrew/navbar/editTitle.navitem.jsx b/client/homebrew/navbar/editTitle.navitem.jsx deleted file mode 100644 index 94ae5d0b0..000000000 --- a/client/homebrew/navbar/editTitle.navitem.jsx +++ /dev/null @@ -1,34 +0,0 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const cx = require('classnames'); -const Nav = require('naturalcrit/nav/nav.jsx'); - -const MAX_TITLE_LENGTH = 50; - - -const EditTitle = createClass({ - displayName : 'EditTitleNavItem', - getDefaultProps : function() { - return { - title : '', - onChange : function(){} - }; - }, - - handleChange : function(e){ - if(e.target.value.length > MAX_TITLE_LENGTH) return; - this.props.onChange(e.target.value); - }, - render : function(){ - return - - -
    = MAX_TITLE_LENGTH })}> - {this.props.title.length}/{MAX_TITLE_LENGTH} -
    -
    ; - }, - -}); - -module.exports = EditTitle; diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx index a6b98af11..f6788e6d5 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -116,17 +116,6 @@ const ErrorNavItem = createClass({ ; } - if(HBErrorCode === '55') { - return - Oops! -
    - Looks like there are too many requests - from this IP address in a short time. - Please try again after a few minutes. -
    -
    ; - } - return Oops!
    diff --git a/client/homebrew/navbar/reddit.navitem.jsx b/client/homebrew/navbar/reddit.navitem.jsx deleted file mode 100644 index 1d9f95604..000000000 --- a/client/homebrew/navbar/reddit.navitem.jsx +++ /dev/null @@ -1,44 +0,0 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const Nav = require('naturalcrit/nav/nav.jsx'); - -const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true'; - - -const RedditShare = createClass({ - displayName : 'RedditShareNavItem', - getDefaultProps : function() { - return { - brew : { - title : '', - sharedId : '', - text : '' - } - }; - }, - - getText : function(){ - - }, - - - handleClick : function(){ - const url = [ - MAIN_URL, - `title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`, - `text=${encodeURIComponent(this.props.brew.text)}` - ].join('&'); - - window.open(url, '_blank'); - }, - - - render : function(){ - return - share on reddit - ; - }, - -}); - -module.exports = RedditShare; diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 039e89dda..fcc43e81a 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -32,7 +32,7 @@ import { updateHistory, versionHistoryGarbageCollection } from '../../utils/vers const googleDriveIcon = require('../../googleDrive.svg'); -const SAVE_TIMEOUT = 16000; +const SAVE_TIMEOUT = 10000; const EditPage = createClass({ displayName : 'EditPage', @@ -228,8 +228,8 @@ const EditPage = createClass({ htmlErrors : Markdown.validate(prevState.brew.text) })); - updateHistory(this.state.brew); - versionHistoryGarbageCollection(); + await updateHistory(this.state.brew); + await versionHistoryGarbageCollection(); const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); diff --git a/client/homebrew/pages/errorPage/errors/errorIndex.js b/client/homebrew/pages/errorPage/errors/errorIndex.js index c5c455bbe..298ec8c7e 100644 --- a/client/homebrew/pages/errorPage/errors/errorIndex.js +++ b/client/homebrew/pages/errorPage/errors/errorIndex.js @@ -172,6 +172,11 @@ const errorIndex = (props)=>{ **Brew Title:** ${props.brew.brewTitle}`, + // ####### Admin page error ####### + '52': dedent` + ## Access Denied + You need to provide correct administrator credentials to access this page.`, + '90' : dedent` An unexpected error occurred while looking for these brews. Try again in a few minutes.`, diff --git a/client/homebrew/pages/sharePage/sharePage.jsx b/client/homebrew/pages/sharePage/sharePage.jsx index 9b4f9b73d..2d96e1ce6 100644 --- a/client/homebrew/pages/sharePage/sharePage.jsx +++ b/client/homebrew/pages/sharePage/sharePage.jsx @@ -25,7 +25,8 @@ const SharePage = createClass({ getInitialState : function() { return { - themeBundle : {} + themeBundle : {}, + currentBrewRendererPageNum : 1 }; }, @@ -39,6 +40,10 @@ const SharePage = createClass({ document.removeEventListener('keydown', this.handleControlKeys); }, + handleBrewRendererPageChange : function(pageNumber){ + this.setState({ currentBrewRendererPageNum: pageNumber }); + }, + handleControlKeys : function(e){ if(!(e.ctrlKey || e.metaKey)) return; const P_KEY = 80; @@ -114,9 +119,12 @@ const SharePage = createClass({
    diff --git a/client/homebrew/pages/vaultPage/vaultPage.jsx b/client/homebrew/pages/vaultPage/vaultPage.jsx index bad1fbd57..a51039345 100644 --- a/client/homebrew/pages/vaultPage/vaultPage.jsx +++ b/client/homebrew/pages/vaultPage/vaultPage.jsx @@ -1,3 +1,5 @@ +/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/ +/*eslint max-params:["warn", { max: 10 }], */ require('./vaultPage.less'); const React = require('react'); @@ -18,13 +20,15 @@ const request = require('../../utils/request-middleware.js'); const VaultPage = (props)=>{ const [pageState, setPageState] = useState(parseInt(props.query.page) || 1); + const [sortState, setSort] = useState(props.query.sort || 'title'); + const [dirState, setdir] = useState(props.query.dir || 'asc'); + //Response state const [brewCollection, setBrewCollection] = useState(null); const [totalBrews, setTotalBrews] = useState(null); const [searching, setSearching] = useState(false); const [error, setError] = useState(null); - const titleRef = useRef(null); const authorRef = useRef(null); const countRef = useRef(null); @@ -34,7 +38,7 @@ const VaultPage = (props)=>{ useEffect(()=>{ disableSubmitIfFormInvalid(); - loadPage(pageState, true); + loadPage(pageState, true, props.query.sort, props.query.dir); }, []); const updateStateWithBrews = (brews, page)=>{ @@ -43,7 +47,7 @@ const VaultPage = (props)=>{ setSearching(false); }; - const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page)=>{ + const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page, sort, dir)=>{ const url = new URL(window.location.href); const urlParams = new URLSearchParams(url.search); @@ -53,21 +57,23 @@ const VaultPage = (props)=>{ urlParams.set('v3', v3Value); urlParams.set('legacy', legacyValue); urlParams.set('page', page); + urlParams.set('sort', sort); + urlParams.set('dir', dir); url.search = urlParams.toString(); window.history.replaceState(null, '', url.toString()); }; - const performSearch = async (title, author, count, v3, legacy, page)=>{ - updateUrl(title, author, count, v3, legacy, page); + const performSearch = async (title, author, count, v3, legacy, page, sort, dir)=>{ + updateUrl(title, author, count, v3, legacy, page, sort, dir); - const response = await request.get( - `/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}` - ).catch((error)=>{ - console.log('error at loadPage: ', error); - setError(error); - updateStateWithBrews([], 1); - }); + const response = await request + .get(`/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}&sort=${sort}&dir=${dir}`) + .catch((error)=>{ + console.log('error at loadPage: ', error); + setError(error); + updateStateWithBrews([], 1); + }); if(response.ok) updateStateWithBrews(response.body.brews, page); @@ -76,9 +82,8 @@ const VaultPage = (props)=>{ const loadTotal = async (title, author, v3, legacy)=>{ setTotalBrews(null); - const response = await request.get( - `/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}` - ).catch((error)=>{ + const response = await request.get(`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}`) + .catch((error)=>{ console.log('error at loadTotal: ', error); setError(error); updateStateWithBrews([], 1); @@ -88,9 +93,8 @@ const VaultPage = (props)=>{ setTotalBrews(response.body.totalBrews); }; - const loadPage = async (page, updateTotal)=>{ - if(!validateForm()) - return; + const loadPage = async (page, updateTotal, sort, dir)=>{ + if(!validateForm()) return; setSearching(true); setError(null); @@ -100,8 +104,14 @@ const VaultPage = (props)=>{ const count = countRef.current.value || 10; const v3 = v3Ref.current.checked != false; const legacy = legacyRef.current.checked != false; + const sortOption = sort || 'title'; + const dirOption = dir || 'asc'; + const pageProp = page || 1; - performSearch(title, author, count, v3, legacy, page); + setSort(sortOption); + setdir(dirOption); + + performSearch(title, author, count, v3, legacy, pageProp, sortOption, dirOption); if(updateTotal) loadTotal(title, author, v3, legacy); @@ -248,6 +258,33 @@ const VaultPage = (props)=>{
    ); + const renderSortOption = (optionTitle, optionValue)=>{ + const oppositeDir = dirState === 'asc' ? 'desc' : 'asc'; + + return ( +
    + + {sortState === optionValue && ( + + )} +
    + ); + }; + + const renderSortBar = ()=>{ + + return ( +
    + {renderSortOption('Title', 'title', props.query.dir)} + {renderSortOption('Created Date', 'createdAt', props.query.dir)} + {renderSortOption('Updated Date', 'updatedAt', props.query.dir)} + {renderSortOption('Views', 'views', props.query.dir)} +
    + ); + }; + const renderPaginationControls = ()=>{ if(!totalBrews) return null; @@ -271,10 +308,8 @@ const VaultPage = (props)=>{ .map((_, index)=>( loadPage(startPage + index, false)} + className={`pageNumber ${pageState === startPage + index ? 'currentPage' : ''}`} + onClick={()=>loadPage(startPage + index, false, sortState, dirState)} > {startPage + index} @@ -284,7 +319,7 @@ const VaultPage = (props)=>{