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/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..b02835726 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,36 @@ + + +## Description + + +## Related Issues or Discussions + +- Closes # + +## QA Instructions, Screenshots, Recordings + +_Please replace this line with instructions on how to test or view your changes, as well as any before/after +images for UI changes._ + +### Reviewer Checklist + +_Please replace the list below with specific features you want reviewers to look at._ + +*Reviewers, refer to this list when testing features, or suggest new items * +- [ ] Verify new features are functional + - [ ] Feature A does X + - [ ] Feature B does Y +- [ ] Verify old features have not broken + - [ ] Feature Z can still be used +- [ ] Test for edge cases / try to break things + - [ ] Feature A handles negative numbers +- [ ] Identify opportunities for simplification and refactoring +- [ ] Check for code legibility and appropriate comments + +
Copy this list 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 f3b284a93..48f155820 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 } = React; +const { useState, useRef, useCallback } = React; const _ = require('lodash'); const MarkdownLegacy = require('naturalcrit/markdownLegacy.js'); @@ -49,23 +49,24 @@ let rawPages = []; const BrewRenderer = (props)=>{ props = { - text : '', - style : '', - renderer : 'legacy', - theme : '5ePHB', - lang : '', - errors : [], - currentEditorPage : 0, - themeBundle : {}, + text : '', + style : '', + renderer : 'legacy', + theme : '5ePHB', + lang : '', + errors : [], + currentEditorCursorPageNum : 1, + currentEditorViewPageNum : 1, + currentBrewRendererPageNum : 1, + themeBundle : {}, + onPageChange : ()=>{}, ...props }; const [state, setState] = useState({ - height : PAGE_HEIGHT, - isMounted : false, - visibility : 'hidden', - zoom : 100, - currentPageNumber : 1, + isMounted : false, + visibility : 'hidden', + zoom : 100 }); const mainRef = useRef(null); @@ -76,36 +77,22 @@ const BrewRenderer = (props)=>{ rawPages = props.text.split(/^\\page$/gm); } - useEffect(()=>{ // Unmounting steps - return ()=>{window.removeEventListener('resize', updateSize);}; - }, []); - - const updateSize = ()=>{ - setState((prevState)=>({ - ...prevState, - height : mainRef.current.parentNode.clientHeight, - })); - }; - - const getCurrentPage = (e)=>{ + const updateCurrentPage = useCallback(_.throttle((e)=>{ const { scrollTop, clientHeight, scrollHeight } = e.target; const totalScrollableHeight = scrollHeight - clientHeight; - const currentPageNumber = Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length); + const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1); - setState((prevState)=>({ - ...prevState, - currentPageNumber : currentPageNumber || 1 - })); - }; + props.onPageChange(currentPageNumber); + }, 200), []); const isInView = (index)=>{ if(!state.isMounted) return false; - if(index == props.currentEditorPage) //Already rendered before this step + if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step return false; - if(Math.abs(index - state.currentPageNumber) <= 3) + if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3) return true; return false; @@ -142,7 +129,7 @@ const BrewRenderer = (props)=>{ renderedPages.length = 0; // Render currently-edited page first so cross-page effects (variables, links) can propagate out first - renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage); + renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1); _.forEach(rawPages, (page, index)=>{ if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){ @@ -164,8 +151,6 @@ const BrewRenderer = (props)=>{ const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount" setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame - updateSize(); - window.addEventListener('resize', updateSize); renderPages(); //Make sure page is renderable before showing setState((prevState)=>({ ...prevState, @@ -188,11 +173,17 @@ 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.*/} {!state.isMounted - ?
+ ?
{renderDummyPage(1)}
@@ -205,7 +196,7 @@ const BrewRenderer = (props)=>{
- + {/*render in iFrame so broken code doesn't crash the site.*/} { 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 9cdaf2ff6..ebeb2ca5f 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx @@ -45,7 +45,7 @@ const NotificationPopup = ()=>{
  • 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! + 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! All PUBLISHED brews will be available to anyone searching there, by title or author, and filtering by renderer. More features will be coming. diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx index fb3b62067..73b48d778 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx +++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx @@ -11,6 +11,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{ const [zoomLevel, setZoomLevel] = useState(100); const [pageNum, setPageNum] = useState(currentPage); + const [toolsVisible, setToolsVisible] = useState(true); useEffect(()=>{ onZoomChange(zoomLevel); @@ -55,7 +56,7 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{ } else if(mode == 'fit'){ // find the page with the largest single dim (height or width) so that zoom can be adapted to fit it. - const minDimRatio = [...pages].reduce((minRatio, page) => Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity); + const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity); desiredZoom = minDimRatio * 100; } @@ -67,7 +68,8 @@ const ToolBar = ({ onZoomChange, currentPage, onPageChange, totalPages })=>{ }; return ( -
    +
    + {/*v=====----------------------< Zoom Controls >---------------------=====v*/}
    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 af493c961..d457d92f2 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -1,10 +1,12 @@ -/*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 { loadHistory } from '../../utils/versionHistory.js'; + //Import all themes const ThemeSnippets = {}; ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js'); @@ -38,7 +40,8 @@ const Snippetbar = createClass({ unfoldCode : ()=>{}, updateEditorTheme : ()=>{}, cursorPos : {}, - snippetBundle : [] + snippetBundle : [], + updateBrew : ()=>{} }; }, @@ -46,31 +49,54 @@ const Snippetbar = createClass({ return { renderer : this.props.renderer, themeSelector : false, - snippets : [] + snippets : [], + 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) { - const snippets = this.compileSnippets(); this.setState({ - snippets : snippets + snippets : this.compileSnippets() + }); + }; + + // 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) { if(key == 'snippets') { const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme - return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property. + return result.filter((snip)=>snip.gen || snip.subsnippets); } }, @@ -138,6 +164,42 @@ const Snippetbar = createClass({ }); }, + replaceContent : function(item){ + return this.props.updateBrew(item); + }, + + toggleHistoryMenu : function(){ + this.setState({ + showHistory : !this.state.showHistory + }); + }, + + renderHistoryItems : function() { + if(!this.state.historyExists) return; + + return
    + {_.map(this.state.historyItems, (item, index)=>{ + if(item.noData || !item.savedAt) return; + + const saveTime = new Date(item.savedAt); + const diffMs = new Date() - saveTime; + const diffSecs = Math.floor(diffMs / 1000); + + let diffString = `about ${diffSecs} seconds ago`; + + if(diffSecs > 60) diffString = `about ${Math.floor(diffSecs / 60)} minutes ago`; + if(diffSecs > (60 * 60)) diffString = `about ${Math.floor(diffSecs / (60 * 60))} hours ago`; + if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`; + if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`; + + return
    {this.replaceContent(item);}} > + + v{item.version} : {diffString} +
    ; + })} +
    ; + }, + renderEditorButtons : function(){ if(!this.props.showEditButtons) return; @@ -158,6 +220,11 @@ const Snippetbar = createClass({ } return
    +
    + + { this.state.showHistory && this.renderHistoryItems() } +
    diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less index e0a24fac9..c50d9df4c 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.less +++ b/client/homebrew/editor/snippetbar/snippetbar.less @@ -53,6 +53,21 @@ font-size : 0.75em; color : inherit; } + &.history { + .tooltipLeft('History'); + font-size : 0.75em; + color : grey; + position : relative; + &.active { + color : inherit; + } + &>.dropdown{ + right : -1px; + &>.snippet{ + padding-right : 10px; + } + } + } &.editorTheme { .tooltipLeft('Editor Themes'); font-size : 0.75em; 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 264d528ef..a6b98af11 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -111,7 +111,7 @@ const ErrorNavItem = createClass({ 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! + {response.body.brewId} still exists!
    ; } diff --git a/client/homebrew/navbar/navbar.less b/client/homebrew/navbar/navbar.less index 4525a193e..c1cda38c3 100644 --- a/client/homebrew/navbar/navbar.less +++ b/client/homebrew/navbar/navbar.less @@ -35,6 +35,11 @@ display : flex; align-items : center; &:last-child .navItem { border-left : 1px solid #666666; } + + &:has(.brewTitle) { + flex-grow : 1; + min-width : 300px; + } } // "NaturalCrit" logo .navLogo { @@ -69,6 +74,10 @@ .navItem { #backgroundColorsHover; .animate(background-color); + display : flex; + align-items : center; + justify-content : center; + height : 100%; padding : 8px 12px; font-size : 10px; font-weight : 800; @@ -94,39 +103,20 @@ animation-duration : 2s; } } - &.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar. - padding : 2px 12px; - input { - width : 250px; - padding : 2px; - margin : 0; - font-family : 'Open Sans', sans-serif; - font-size : 12px; - font-weight : 800; - color : white; - text-align : center; - background-color : transparent; - border : 1px solid @blue; - outline : none; - } - .charCount { - display : inline-block; - margin-left : 8px; - color : #666666; - text-align : right; - vertical-align : bottom; - &.max { color : @red; } - } - } &.brewTitle { - flex-grow : 1; + display : block; + width : 100%; + overflow : hidden; font-size : 12px; font-weight : 800; color : white; text-align : center; - text-transform : initial; - background-color : transparent; + text-overflow : ellipsis; + text-transform : initial; + white-space : nowrap; + background-color : transparent; } + // "The Homebrewery" logo &.homebrewLogo { .animate(color); @@ -240,23 +230,25 @@ } .navDropdownContainer { position : relative; + height : 100%; + .navDropdown { - position: absolute; - top: 28px; - right: 0px; - z-index: 10000; - width: max-content; - min-width:100%; - max-height: calc(100vh - 28px); - overflow: hidden auto; - display: flex; - flex-direction: column; - align-items: flex-end; + position : absolute; + //top: 28px; + right : 0px; + z-index : 10000; + display : flex; + flex-direction : column; + align-items : flex-end; + width : max-content; + min-width : 100%; + max-height : calc(100vh - 28px); + overflow : hidden auto; .navItem { position : relative; display : flex; - justify-content : space-between; align-items : center; + justify-content : space-between; width : 100%; border : 1px solid #888888; border-bottom : 0; @@ -278,10 +270,10 @@ overflow : hidden auto; color : white; text-decoration : none; - background-color : #333333; - border-top : 1px solid #888888; scrollbar-color : #666666 #333333; scrollbar-width : thin; + background-color : #333333; + border-top : 1px solid #888888; .clear { position : absolute; top : 50%; 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 4d7741000..548c26ec5 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,8 +1,9 @@ /* eslint-disable max-lines */ require('./editPage.less'); const React = require('react'); -const createClass = require('create-react-class'); const _ = require('lodash'); +const createClass = require('create-react-class'); + const request = require('../../utils/request-middleware.js'); const { Meta } = require('vitreum/headtags'); @@ -27,6 +28,8 @@ const Markdown = require('naturalcrit/markdown.js'); const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); +import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; + const googleDriveIcon = require('../../googleDrive.svg'); const SAVE_TIMEOUT = 16000; @@ -41,22 +44,24 @@ const EditPage = createClass({ getInitialState : function() { return { - brew : this.props.brew, - isSaving : false, - isPending : 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(), - currentEditorPage : 0, - displayLockMessage : this.props.brew.lock || false, - themeBundle : {} + brew : this.props.brew, + isSaving : false, + isPending : 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 : {} }; }, @@ -113,16 +118,27 @@ const EditPage = createClass({ this.editor.current.update(); }, + handleEditorViewPageChange : function(pageNumber){ + this.setState({ currentEditorViewPageNum: pageNumber }); + }, + + handleEditorCursorPageChange : function(pageNumber){ + this.setState({ currentEditorCursorPageNum: pageNumber }); + }, + + 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 }, - isPending : true, - htmlErrors : htmlErrors, - currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 + brew : { ...prevState.brew, text: text }, + isPending : true, + htmlErrors : htmlErrors, }), ()=>{if(this.state.autoSave) this.trySave();}); }, @@ -150,6 +166,16 @@ const EditPage = createClass({ return !_.isEqual(this.state.brew, this.savedBrew); }, + updateBrew : function(newData){ + this.setState((prevState)=>({ + brew : { + ...prevState.brew, + style : newData.style, + text : newData.text + } + })); + }, + trySave : function(immediate=false){ if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); if(this.hasChanges()){ @@ -202,6 +228,9 @@ const EditPage = createClass({ htmlErrors : Markdown.validate(prevState.brew.text) })); + await updateHistory(this.state.brew); + await versionHistoryGarbageCollection(); + const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); const brew = this.state.brew; @@ -413,6 +442,12 @@ const EditPage = createClass({ renderer={this.state.brew.renderer} userThemes={this.props.userThemes} snippetBundle={this.state.themeBundle.snippets} + updateBrew={this.updateBrew} + onCursorPageChange={this.handleEditorCursorPageChange} + onViewPageChange={this.handleEditorViewPageChange} + currentEditorViewPageNum={this.state.currentEditorViewPageNum} + currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} + currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} /> 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/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index d7efcaf14..ac3be81df 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -1,7 +1,6 @@ require('./homePage.less'); const React = require('react'); const createClass = require('create-react-class'); -const _ = require('lodash'); const cx = require('classnames'); const request = require('../../utils/request-middleware.js'); const { Meta } = require('vitreum/headtags'); @@ -32,11 +31,13 @@ const HomePage = createClass({ }, getInitialState : function() { return { - brew : this.props.brew, - welcomeText : this.props.brew.text, - error : undefined, - currentEditorPage : 0, - themeBundle : {} + brew : this.props.brew, + welcomeText : this.props.brew.text, + error : undefined, + currentEditorViewPageNum : 1, + currentEditorCursorPageNum : 1, + currentBrewRendererPageNum : 1, + themeBundle : {} }; }, @@ -61,10 +62,22 @@ const HomePage = createClass({ handleSplitMove : function(){ this.editor.current.update(); }, + + handleEditorViewPageChange : function(pageNumber){ + this.setState({ currentEditorViewPageNum: pageNumber }); + }, + + handleEditorCursorPageChange : function(pageNumber){ + this.setState({ currentEditorCursorPageNum: pageNumber }); + }, + + handleBrewRendererPageChange : function(pageNumber){ + this.setState({ currentBrewRendererPageNum: pageNumber }); + }, + handleTextChange : function(text){ this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 + brew : { ...prevState.brew, text: text }, })); }, renderNavbar : function(){ @@ -97,12 +110,20 @@ const HomePage = createClass({ renderer={this.state.brew.renderer} showEditButtons={false} snippetBundle={this.state.themeBundle.snippets} + onCursorPageChange={this.handleEditorCursorPageChange} + onViewPageChange={this.handleEditorViewPageChange} + currentEditorViewPageNum={this.state.currentEditorViewPageNum} + currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} + currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} /> diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 5b0f59c00..c147cd474 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -39,13 +39,15 @@ const NewPage = createClass({ 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), - currentEditorPage : 0, - themeBundle : {} + 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 : {} }; }, @@ -108,15 +110,26 @@ const NewPage = createClass({ this.editor.current.update(); }, + handleEditorViewPageChange : function(pageNumber){ + this.setState({ currentEditorViewPageNum: pageNumber }); + }, + + handleEditorCursorPageChange : function(pageNumber){ + this.setState({ currentEditorCursorPageNum: pageNumber }); + }, + + 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, - currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0 + brew : { ...prevState.brew, text: text }, + htmlErrors : htmlErrors, })); localStorage.setItem(BREWKEY, text); }, @@ -221,6 +234,11 @@ const NewPage = createClass({ renderer={this.state.brew.renderer} userThemes={this.props.userThemes} snippetBundle={this.state.themeBundle.snippets} + onCursorPageChange={this.handleEditorCursorPageChange} + onViewPageChange={this.handleEditorViewPageChange} + currentEditorViewPageNum={this.state.currentEditorViewPageNum} + currentEditorCursorPageNum={this.state.currentEditorCursorPageNum} + currentBrewRendererPageNum={this.state.currentBrewRendererPageNum} /> 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 a550ec578..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)=>{