diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index 2226c4f3f..63cf295fe 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -10,6 +10,7 @@ const UserPage = require('./pages/userPage/userPage.jsx'); const SharePage = require('./pages/sharePage/sharePage.jsx'); const NewPage = require('./pages/newPage/newPage.jsx'); const ErrorPage = require('./pages/errorPage/errorPage.jsx'); +const VaultPage = require('./pages/vaultPage/vaultPage.jsx'); const AccountPage = require('./pages/accountPage/accountPage.jsx'); const WithRoute = (props)=>{ @@ -71,6 +72,7 @@ const Homebrew = createClass({ } /> } /> } /> + }/> } /> } /> } /> diff --git a/client/homebrew/navbar/vault.navitem.jsx b/client/homebrew/navbar/vault.navitem.jsx new file mode 100644 index 000000000..087297011 --- /dev/null +++ b/client/homebrew/navbar/vault.navitem.jsx @@ -0,0 +1,17 @@ +const React = require('react'); + +const Nav = require('naturalcrit/nav/nav.jsx'); + +module.exports = function (props) { + return ( + + Vault + + ); +}; diff --git a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx index bf0624f1c..039bc98f5 100644 --- a/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx +++ b/client/homebrew/pages/basePages/listPage/brewItem/brewItem.jsx @@ -19,7 +19,8 @@ const BrewItem = createClass({ stubbed : true }, updateListFilter : ()=>{}, - reportError : ()=>{} + reportError : ()=>{}, + renderStorage : true }; }, @@ -95,6 +96,7 @@ const BrewItem = createClass({ }, renderStorageIcon : function(){ + if(!this.props.renderStorage) return; if(this.props.brew.googleId) { return @@ -142,10 +144,14 @@ const BrewItem = createClass({ } {brew.authors?.map((author, index)=>( - <> - {author} - {index < brew.authors.length - 1 && ', '} - ))} + + {author === 'hidden' + ? {author} + : {author} + } + {index < brew.authors.length - 1 && ', '} + + ))}
diff --git a/client/homebrew/pages/errorPage/errors/errorIndex.js b/client/homebrew/pages/errorPage/errors/errorIndex.js index 957991ad6..c5c455bbe 100644 --- a/client/homebrew/pages/errorPage/errors/errorIndex.js +++ b/client/homebrew/pages/errorPage/errors/errorIndex.js @@ -161,7 +161,7 @@ const errorIndex = (props)=>{ Please login or signup at our [login page](https://www.naturalcrit.com/login?redirect=https://homebrewery.naturalcrit.com/account).`, // Brew locked by Administrators error - '100' : dedent` + '51' : dedent` ## This brew has been locked. Only an author may request that this lock is removed. @@ -171,6 +171,11 @@ const errorIndex = (props)=>{ **Brew ID:** ${props.brew.brewId} **Brew Title:** ${props.brew.brewTitle}`, + + '90' : dedent` An unexpected error occurred while looking for these brews. + Try again in a few minutes.`, + + '91' : dedent` An unexpected error occurred while trying to get the total of brews.`, }; }; diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 490b22596..d7efcaf14 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -10,12 +10,12 @@ const Nav = require('naturalcrit/nav/nav.jsx'); const Navbar = require('../../navbar/navbar.jsx'); const NewBrewItem = require('../../navbar/newbrew.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx'); +const VaultNavItem = require('../../navbar/vault.navitem.jsx'); const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; const AccountNavItem = require('../../navbar/account.navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx'); const { fetchThemeBundle } = require('../../../../shared/helpers.js'); - const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const Editor = require('../../editor/editor.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); @@ -76,6 +76,7 @@ const HomePage = createClass({ } + diff --git a/client/homebrew/pages/userPage/userPage.jsx b/client/homebrew/pages/userPage/userPage.jsx index 01778be44..d6fe25b30 100644 --- a/client/homebrew/pages/userPage/userPage.jsx +++ b/client/homebrew/pages/userPage/userPage.jsx @@ -12,6 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx'); const NewBrew = require('../../navbar/newbrew.navitem.jsx'); const HelpNavItem = require('../../navbar/help.navitem.jsx'); const ErrorNavItem = require('../../navbar/error-navitem.jsx'); +const VaultNavitem = require('../../navbar/vault.navitem.jsx'); const UserPage = createClass({ displayName : 'UserPage', @@ -66,6 +67,7 @@ const UserPage = createClass({ } + diff --git a/client/homebrew/pages/vaultPage/vaultPage.jsx b/client/homebrew/pages/vaultPage/vaultPage.jsx new file mode 100644 index 000000000..01356d7da --- /dev/null +++ b/client/homebrew/pages/vaultPage/vaultPage.jsx @@ -0,0 +1,395 @@ +require('./vaultPage.less'); + +const React = require('react'); +const { useState, useEffect, useRef } = React; + +const Nav = require('naturalcrit/nav/nav.jsx'); +const Navbar = require('../../navbar/navbar.jsx'); +const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; +const Account = require('../../navbar/account.navitem.jsx'); +const NewBrew = require('../../navbar/newbrew.navitem.jsx'); +const HelpNavItem = require('../../navbar/help.navitem.jsx'); +const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx'); +const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx'); +const ErrorIndex = require('../errorPage/errors/errorIndex.js'); + +const request = require('../../utils/request-middleware.js'); + +const VaultPage = (props)=>{ + const [pageState, setPageState] = useState(parseInt(props.query.page) || 1); + + //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); + const v3Ref = useRef(null); + const legacyRef = useRef(null); + const submitButtonRef = useRef(null); + + useEffect(()=>{ + disableSubmitIfFormInvalid(); + loadPage(pageState, true); + }, []); + + const updateStateWithBrews = (brews, page)=>{ + setBrewCollection(brews || null); + setPageState(parseInt(page) || 1); + setSearching(false); + }; + + const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page)=>{ + const url = new URL(window.location.href); + const urlParams = new URLSearchParams(url.search); + + urlParams.set('title', titleValue); + urlParams.set('author', authorValue); + urlParams.set('count', countValue); + urlParams.set('v3', v3Value); + urlParams.set('legacy', legacyValue); + urlParams.set('page', page); + + 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 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); + }); + + if(response.ok) + updateStateWithBrews(response.body.brews, page); + }; + + 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)=>{ + console.log('error at loadTotal: ', error); + setError(error); + updateStateWithBrews([], 1); + }); + + if(response.ok) + setTotalBrews(response.body.totalBrews); + }; + + const loadPage = async (page, updateTotal)=>{ + if(!validateForm()) + return; + + setSearching(true); + setError(null); + + const title = titleRef.current.value || ''; + const author = authorRef.current.value || ''; + const count = countRef.current.value || 10; + const v3 = v3Ref.current.checked != false; + const legacy = legacyRef.current.checked != false; + + performSearch(title, author, count, v3, legacy, page); + + if(updateTotal) + loadTotal(title, author, v3, legacy); + }; + + const renderNavItems = ()=>( + + + + Vault: Search for brews + + + + + + + + + + ); + + const validateForm = ()=>{ + //form validity: title or author must be written, and at least one renderer set + const isTitleValid = titleRef.current.validity.valid && titleRef.current.value; + const isAuthorValid = authorRef.current.validity.valid && authorRef.current.value; + const isCheckboxChecked = legacyRef.current.checked || v3Ref.current.checked; + + const isFormValid = (isTitleValid || isAuthorValid) && isCheckboxChecked; + + return isFormValid; + }; + + const disableSubmitIfFormInvalid = ()=>{ + submitButtonRef.current.disabled = !validateForm(); + }; + + const renderForm = ()=>( +
+

Brew Lookup

+
+ + + + + + + + + + + +
+ +

Tips and tricks

+
    +
  • + Only published brews are searchable via this tool +
  • +
  • + Usernames are case-sensitive +
  • +
  • + Use "word" to match an exact string, + and - to exclude words (at least one word must not be negated) +
  • +
  • + Some common words like "a", "after", "through", "itself", "here", etc., + are ignored in searches. The full list can be found   + + here + +
  • +
+
+
+ ); + + const renderPaginationControls = ()=>{ + if(!totalBrews) return null; + + const countInt = parseInt(props.query.count || 20); + const totalPages = Math.ceil(totalBrews / countInt); + + let startPage, endPage; + if(pageState <= 6) { + startPage = 1; + endPage = Math.min(totalPages, 10); + } else if(pageState + 4 >= totalPages) { + startPage = Math.max(1, totalPages - 9); + endPage = totalPages; + } else { + startPage = pageState - 5; + endPage = pageState + 4; + } + + const pagesAroundCurrent = new Array(endPage - startPage + 1) + .fill() + .map((_, index)=>( + loadPage(startPage + index, false)} + > + {startPage + index} + + )); + + return ( +
+ +
    + {startPage > 1 && ( + loadPage(1, false)} + > + 1 ... + + )} + {pagesAroundCurrent} + {endPage < totalPages && ( + loadPage(totalPages, false)} + > + ... {totalPages} + + )} +
+ +
+ ); + }; + + const renderFoundBrews = ()=>{ + if(searching) { + return ( +
+

Searching

+
+ ); + } + + if(error) { + const errorText = ErrorIndex()[error.HBErrorCode.toString()] || ''; + + return ( +
+

Error: {errorText}

+
+ ); + } + + if(!brewCollection) { + return ( +
+

No search yet

+
+ ); + } + + if(brewCollection.length === 0) { + return ( +
+

No brews found

+
+ ); + } + + return ( +
+ + {`Brews found: `} + {totalBrews} + + {brewCollection.map((brew, index)=>{ + return ( + + ); + })} + {renderPaginationControls()} +
+ ); + }; + + return ( +
+ + + {renderNavItems()} +
+ +
{renderForm()}
+ +
+ {renderFoundBrews()} +
+
+
+
+ ); +}; + +module.exports = VaultPage; diff --git a/client/homebrew/pages/vaultPage/vaultPage.less b/client/homebrew/pages/vaultPage/vaultPage.less new file mode 100644 index 000000000..921fa7c08 --- /dev/null +++ b/client/homebrew/pages/vaultPage/vaultPage.less @@ -0,0 +1,367 @@ +body { + height : 100vh; + + .content { height : 100%; } + + small { + font-size : 10pt; + color : #555555; + + a { color : #333333; } + } + + code { + padding-inline : 5px; + background : lightgrey; + border-radius : 5px; + } + + *:not(input) { user-select : none; } +} + +.vaultPage { + height : 100%; + overflow-y : hidden; + background-color : #2C3E50; + + + .content { + background : #2C3E50; + + .dataGroup { + width : 100%; + height : 100%; + background : white; + + &.form .brewLookup { + position : relative; + padding : 50px clamp(20px, 4vw, 50px); + + h1, h2, h3, h4 { + font-family : 'CodeBold'; + letter-spacing : 2px; + } + + + legend { + h3 { + margin-block : 30px 20px; + font-size : 20px; + text-align : center; + border-bottom : 2px solid; + } + ul { + padding-inline : 30px 10px; + li { + margin-block : 5px; + line-height : calc(1em + 5px); + list-style : disc; + } + } + } + + &::after { + position : absolute; + top : 0; + right : 0; + left : 0; + display : block; + padding : 10px; + font-weight : 900; + color : white; + white-space : pre-wrap; + content : 'Error:\A At least one renderer should be enabled to make a search'; + background : rgb(255, 60, 60); + opacity : 0; + transition : opacity 0.5s; + } + &:not(:has(input[type='checkbox']:checked))::after { opacity : 1; } + + .formTitle { + margin : 20px 0; + font-size : 30px; + color : black; + text-align : center; + border-bottom : 2px solid; + } + + .formContents { + position : relative; + display : flex; + flex-direction : column; + + label { + display : flex; + align-items : center; + margin : 10px 0; + } + select { margin : 0 10px; } + + input { + margin : 0 10px; + + &:invalid { background : rgb(255, 188, 181); } + + &[type='checkbox'] { + position : relative; + display : inline-block; + width : 50px; + height : 30px; + font-family : 'WalterTurncoat'; + font-size : 20px; + font-weight : 800; + color : white; + letter-spacing : 2px; + appearance : none; + background : red; + isolation : isolate; + border-radius : 5px; + + &::before,&::after { + position : absolute; + inset : 0; + z-index : 5; + padding-top : 2px; + text-align : center; + } + + &::before { + display : block; + content : 'No'; + } + + &::after { + display : none; + content : 'Yes'; + } + + &:checked { + background : green; + + &::before { display : none; } + &::after { display : block; } + } + } + } + + + + #searchButton { + position : absolute; + right : 20px; + bottom : 0; + + i { + margin-left : 10px; + animation-duration : 1000s; + } + } + } + } + + &.resultsContainer { + display : flex; + flex-direction : column; + height : 100%; + overflow-y : auto; + font-family : 'BookInsanityRemake'; + font-size : 0.34cm; + + + h3 { + font-family : 'Open Sans'; + font-weight : 900; + color : white; + } + + .foundBrews { + position : relative; + width : 100%; + height : 100%; + max-height : 100%; + padding : 50px 50px 70px 50px; + overflow-y : scroll; + background-color : #2C3E50; + + h3 { font-size : 25px; } + + &.noBrews { + display : grid; + place-items : center; + color : white; + } + + &.searching { + display : grid; + place-items : center; + color : white; + + h3 { position : relative; } + + h3.searchAnim::after { + position : absolute; + top : 50%; + right : 0; + width : max-content; + height : 1em; + content : ''; + translate : calc(100% + 5px) -50%; + animation : trailingDots 2s ease infinite; + } + } + + .totalBrews { + position : fixed; + right : 0; + bottom : 0; + z-index : 1000; + padding : 8px 10px; + font-family : 'Open Sans'; + font-size : 11px; + font-weight : 800; + color : white; + background-color : #333333; + + .searchAnim { + position : relative; + display : inline-block; + width : 3ch; + height : 1em; + } + + .searchAnim::after { + position : absolute; + top : 50%; + right : 0; + width : max-content; + height : 1em; + content : ''; + translate : -50% -50%; + animation : trailingDots 2s ease infinite; + } + } + + .brewItem { + width : 47%; + margin-right : 40px; + color : black; + background-image : url('/assets/parchmentBackground.jpg'); + + &:nth-child(even of .brewItem) { margin-right : 0; } + + h2 { + font-family : 'MrEavesRemake'; + font-size : 0.75cm; + font-weight : 800; + line-height : 0.988em; + color : var(--HB_Color_HeaderText); + } + + .text { + position:relative; + z-index:2; + } + .info { + font-family : 'ScalySansRemake'; + font-size : 1.2em; + position:relative; + z-index:2; + + >span { + margin-right : 12px; + line-height : 1.5em; + } + } + .links { + z-index:2; + } + + hr { + margin: 0px; + visibility: hidden; + } + + .thumbnail { + z-index:1; + } + } + + .paginationControls { + position : absolute; + left : 50%; + display : grid; + grid-template-areas : 'previousPage currentPage nextPage'; + grid-template-columns : 50px 1fr 50px; + place-items : center; + width : auto; + translate : -50%; + + .pages { + display : flex; + grid-area : currentPage; + justify-content : space-evenly; + width : 100%; + height : 100%; + padding : 5px 8px; + text-align : center; + + .pageNumber { + margin-inline : 1vw; + font-family : 'Open Sans'; + font-weight : 900; + color : white; + text-underline-position : under; + text-wrap : nowrap; + cursor : pointer; + + &.currentPage { + color : gold; + text-decoration : underline; + pointer-events : none; + } + + &.firstPage { margin-right : -5px; } + + &.lastPage { margin-left : -5px; } + } + } + + button { + width : max-content; + + &.previousPage { grid-area : previousPage; } + + &.nextPage { grid-area : nextPage; } + } + + } + } + } + } + } +} + +@keyframes trailingDots { + + 0%, + 32% { content : ' .'; } + + 33%, + 65% { content : ' ..'; } + + 66%, + 100% { content : ' ...'; } +} + +// media query for when the page is smaller than 1079 px in width +@media screen and (max-width : 1079px) { + .vaultPage .content { + + .dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; } + + .dataGroup.resultsContainer .foundBrews .brewItem { + width : 100%; + margin-inline : auto; + } + } +} diff --git a/server/app.js b/server/app.js index 90d14aa5b..02075d31d 100644 --- a/server/app.js +++ b/server/app.js @@ -55,6 +55,7 @@ app.use((req, res, next)=>{ app.use(homebrewApi); app.use(require('./admin.api.js')); +app.use(require('./vault.api.js')); const HomebrewModel = require('./homebrew.model.js').model; const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8'); @@ -433,6 +434,11 @@ if(isLocalEnvironment){ }); } +//Vault Page +app.get('/vault', asyncHandler(async(req, res, next)=>{ + return next(); +})); + //Send rendered page app.use(asyncHandler(async (req, res, next)=>{ if (!req.route) return res.redirect('/'); // Catch-all for invalid routes diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 52fe57360..f15376af7 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -99,7 +99,7 @@ const api = { stub = stub?.toObject(); if(stub?.lock?.locked && accessType != 'edit') { - throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; + throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title }; } // If there is a google id, try to find the google brew diff --git a/server/homebrew.api.spec.js b/server/homebrew.api.spec.js index d168c73fb..6e7c36641 100644 --- a/server/homebrew.api.spec.js +++ b/server/homebrew.api.spec.js @@ -309,7 +309,7 @@ describe('Tests for api', ()=>{ const req = { brew: {} }; const next = jest.fn(); - await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' }); + await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' }); }); }); diff --git a/server/vault.api.js b/server/vault.api.js new file mode 100644 index 000000000..41ceeab8e --- /dev/null +++ b/server/vault.api.js @@ -0,0 +1,102 @@ +const express = require('express'); +const asyncHandler = require('express-async-handler'); +const HomebrewModel = require('./homebrew.model.js').model; + +const router = express.Router(); + +const titleConditions = (title)=>{ + if(!title) return {}; + return { + $text : { + $search : title, + $caseSensitive : false, + }, + }; +}; + +const authorConditions = (author)=>{ + if(!author) return {}; + return { authors: author }; +}; + +const rendererConditions = (legacy, v3)=>{ + if(legacy === 'true' && v3 !== 'true') + return { renderer: 'legacy' }; + + if(v3 === 'true' && legacy !== 'true') + return { renderer: 'V3' }; + + return {}; // If all renderers selected, renderer field not needed in query for speed +}; + +const findBrews = async (req, res)=>{ + const title = req.query.title || ''; + const author = req.query.author || ''; + const page = Math.max(parseInt(req.query.page) || 1, 1); + const count = Math.max(parseInt(req.query.count) || 20, 10); + const skip = (page - 1) * count; + + const combinedQuery = { + $and : [ + { published: true }, + rendererConditions(req.query.legacy, req.query.v3), + titleConditions(title), + authorConditions(author) + ], + }; + + const projection = { + editId : 0, + googleId : 0, + text : 0, + textBin : 0, + version : 0 + }; + + await HomebrewModel.find(combinedQuery, projection) + .skip(skip) + .limit(count) + .maxTimeMS(5000) + .exec() + .then((brews)=>{ + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + const processedBrews = brews.map((brew)=>{ + brew.authors = brew.authors.map((author)=>emailRegex.test(author) ? 'hidden' : author + ); + return brew; + }); + res.json({ brews: processedBrews, page }); + }) + .catch((error)=>{ + throw { ...error, message: 'Error finding brews in Vault search', HBErrorCode: 90 }; + }); +}; + +const findTotal = async (req, res)=>{ + const title = req.query.title || ''; + const author = req.query.author || ''; + + const combinedQuery = { + $and : [ + { published: true }, + rendererConditions(req.query.legacy, req.query.v3), + titleConditions(title), + authorConditions(author) + ], + }; + + await HomebrewModel.countDocuments(combinedQuery) + .then((totalBrews)=>{ + console.log(`when returning, the total of brews is ${totalBrews} for the query ${JSON.stringify(combinedQuery)}`); + res.json({ totalBrews }); + }) + .catch((error)=>{ + throw { ...error, message: 'Error finding brews in Vault search findTotal function', HBErrorCode: 91 }; + }); +}; + +router.get('/api/vault/total', asyncHandler(findTotal)); +router.get('/api/vault', asyncHandler(findBrews)); + +module.exports = router; diff --git a/shared/naturalcrit/splitPane/splitPane.jsx b/shared/naturalcrit/splitPane/splitPane.jsx index 55af5e386..32f183a99 100644 --- a/shared/naturalcrit/splitPane/splitPane.jsx +++ b/shared/naturalcrit/splitPane/splitPane.jsx @@ -7,8 +7,9 @@ const SplitPane = createClass({ displayName : 'SplitPane', getDefaultProps : function() { return { - storageKey : 'naturalcrit-pane-split', - onDragFinish : function(){} //fires when dragging + storageKey : 'naturalcrit-pane-split', + onDragFinish : function(){}, //fires when dragging + showDividerButtons : true }; }, @@ -18,8 +19,7 @@ const SplitPane = createClass({ windowWidth : 0, isDragging : false, moveSource : false, - moveBrew : false, - showMoveArrows : true + moveBrew : false }; }, @@ -142,9 +142,11 @@ const SplitPane = createClass({ width={this.state.currentDividerPos} > {React.cloneElement(this.props.children[0], { - moveBrew : this.state.moveBrew, - moveSource : this.state.moveSource, - setMoveArrows : this.setMoveArrows + ...(this.props.showDividerButtons && { + moveBrew: this.state.moveBrew, + moveSource: this.state.moveSource, + setMoveArrows: this.setMoveArrows, + }), })} {this.renderDivider()}