diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index 51196a444..00b88346f 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -1,449 +1,375 @@ /* eslint-disable max-lines */ -require('./editPage.less'); -const React = require('react'); -const _ = require('lodash'); -const createClass = require('create-react-class'); -import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; -import { md5 } from 'hash-wasm'; -import { gzipSync, strToU8 } from 'fflate'; +import './editPage.less'; + +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import request from '../../utils/request-middleware.js'; +import Markdown from 'naturalcrit/markdown.js'; + +import _ from 'lodash';; +import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch'; +import { md5 } from 'hash-wasm'; +import { gzipSync, strToU8 } from 'fflate'; -import request from '../../utils/request-middleware.js'; const { Meta } = require('vitreum/headtags'); -const Nav = require('naturalcrit/nav/nav.jsx'); -const Navbar = require('../../navbar/navbar.jsx'); +import Nav from 'naturalcrit/nav/nav.jsx'; +import Navbar from '../../navbar/navbar.jsx'; +import NewBrewItem from '../../navbar/newbrew.navitem.jsx'; +import AccountNavItem from '../../navbar/account.navitem.jsx'; +import ErrorNavItem from '../../navbar/error-navitem.jsx'; +import HelpNavItem from '../../navbar/help.navitem.jsx'; +import VaultNavItem from '../../navbar/vault.navitem.jsx'; +import PrintNavItem from '../../navbar/print.navitem.jsx'; +import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx'; -const NewBrew = require('../../navbar/newbrew.navitem.jsx'); -const HelpNavItem = require('../../navbar/help.navitem.jsx'); -const PrintNavItem = require('../../navbar/print.navitem.jsx'); -const ErrorNavItem = require('../../navbar/error-navitem.jsx'); -const Account = require('../../navbar/account.navitem.jsx'); -const RecentNavItem = require('../../navbar/recent.navitem.jsx').both; -const VaultNavItem = require('../../navbar/vault.navitem.jsx'); +import SplitPane from 'client/components/splitPane/splitPane.jsx'; +import Editor from '../../editor/editor.jsx'; +import BrewRenderer from '../../brewRenderer/brewRenderer.jsx'; -const SplitPane = require('client/components/splitPane/splitPane.jsx'); -const Editor = require('../../editor/editor.jsx'); -const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); +import LockNotification from './lockNotification/lockNotification.jsx'; -const LockNotification = require('./lockNotification/lockNotification.jsx'); - -import Markdown from 'naturalcrit/markdown.js'; - -const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js'); -const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js'); +import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; +import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js'; import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js'; -const googleDriveIcon = require('../../googleDrive.svg'); +import googleDriveIcon from '../../googleDrive.svg'; const SAVE_TIMEOUT = 10000; -const EditPage = createClass({ - displayName : 'EditPage', - getDefaultProps : function() { - return { - brew : DEFAULT_BREW_LOAD - }; - }, +const EditPage = (props) => { + props = { + brew: DEFAULT_BREW_LOAD, + ...props + }; + const editor = useRef(null); + const savedBrew = useRef(_.cloneDeep(props.brew)); + const warningTimer = useRef(null); - getInitialState : function() { - return { - brew : this.props.brew, - isSaving : false, - unsavedChanges : false, - alertTrashedGoogleBrew : this.props.brew.trashed, - alertLoginToTransfer : false, - saveGoogle : this.props.brew.googleId ? true : false, - confirmGoogleTransfer : false, - error : null, - htmlErrors : Markdown.validate(this.props.brew.text), - url : '', - autoSave : true, - autoSaveWarning : false, - unsavedTime : new Date(), - currentEditorViewPageNum : 1, - currentEditorCursorPageNum : 1, - currentBrewRendererPageNum : 1, - displayLockMessage : this.props.brew.lock || false, - themeBundle : {} - }; - }, + const [currentBrew , setCurrentBrew ] = useState(props.brew); + const [isSaving , setIsSaving ] = useState(false); + const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); + const [error , setError ] = useState(null); + const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); + const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); + const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); + const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); + const [themeBundle , setThemeBundle ] = useState({}); + const [unsavedChanges , setUnsavedChanges ] = useState(false); + const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed); + const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); + const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); + const [url , setUrl ] = useState(''); + const [autoSave , setAutoSave ] = useState(true); + const [autoSaveWarning , setAutoSaveWarning ] = useState(false); + const [unsavedTime , setUnsavedTime ] = useState(new Date()); + const [displayLockMessage , setDisplayLockMessage ] = useState(props.brew.lock || false); - editor : React.createRef(null), - savedBrew : null, + const debounceSave = useMemo(() => _.debounce(() => trySave(), SAVE_TIMEOUT), []); - componentDidMount : function(){ - this.setState({ - url : window.location.href - }); + useEffect(() => { + setUrl(window.location.href); - this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy + const autoSavePref = JSON.parse(localStorage.getItem('AUTOSAVE_ON') ?? true); + setAutoSave(autoSavePref); + setAutoSaveWarning(!autoSavePref) + setHTMLErrors(Markdown.validate(currentBrew.text)); + fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme); - this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{ - if(this.state.autoSave){ - this.trySave(); - } else { - this.setState({ autoSaveWarning: true }); - } - }); - - window.onbeforeunload = ()=>{ - if(this.state.isSaving || this.state.unsavedChanges){ + document.addEventListener('keydown', handleControlKeys); + window.onbeforeunload = () => { + if (isSaving || unsavedChanges) { return 'You have unsaved changes!'; } }; - this.setState((prevState)=>({ - htmlErrors : Markdown.validate(prevState.brew.text) - })); + return () => { + document.removeEventListener('keydown', handleControlKeys); + window.onbeforeunload = null; + }; + }, []); - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme); + useEffect(() => { + const hasChange = !_.isEqual(currentBrew, savedBrew.current); + setUnsavedChanges(hasChange); + }, [currentBrew]); - document.addEventListener('keydown', this.handleControlKeys); - }, - componentWillUnmount : function() { - window.onbeforeunload = function(){}; - document.removeEventListener('keydown', this.handleControlKeys); - }, - componentDidUpdate : function(){ - const hasChange = this.hasChanges(); - if(this.state.unsavedChanges != hasChange){ - this.setState({ - unsavedChanges : hasChange - }); - } - }, - - handleControlKeys : function(e){ - if(!(e.ctrlKey || e.metaKey)) return; + const handleControlKeys = (e) => { + if (!(e.ctrlKey || e.metaKey)) return; const S_KEY = 83; const P_KEY = 80; - if(e.keyCode == S_KEY) this.trySave(true); - if(e.keyCode == P_KEY) printCurrentBrew(); - if(e.keyCode == P_KEY || e.keyCode == S_KEY){ + if (e.keyCode === S_KEY) trySave(true); + if (e.keyCode === P_KEY) printCurrentBrew(); + if (e.keyCode === S_KEY || e.keyCode === P_KEY) { e.stopPropagation(); e.preventDefault(); } - }, + }; - handleSplitMove : function(){ - this.editor.current.update(); - }, + const handleSplitMove = () => { + editor.current?.update(); + }; - handleEditorViewPageChange : function(pageNumber){ - this.setState({ currentEditorViewPageNum: pageNumber }); - }, + const handleEditorViewPageChange = (pageNumber) => { + setCurrentEditorViewPageNum(pageNumber); + }; - handleEditorCursorPageChange : function(pageNumber){ - this.setState({ currentEditorCursorPageNum: pageNumber }); - }, + const handleEditorCursorPageChange = (pageNumber) => { + setCurrentEditorCursorPageNum(pageNumber); + }; - handleBrewRendererPageChange : function(pageNumber){ - this.setState({ currentBrewRendererPageNum: pageNumber }); - }, + const handleBrewRendererPageChange = (pageNumber) => { + setCurrentBrewRendererPageNum(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); + const handleTextChange = (text) => { + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(text); - this.setState((prevState)=>({ - brew : { ...prevState.brew, text: text }, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, text })); + if (autoSave) debounceSave(); + }; - handleSnipChange : function(snippet){ - //If there are errors, run the validator on every change to give quick feedback - let htmlErrors = this.state.htmlErrors; - if(htmlErrors.length) htmlErrors = Markdown.validate(snippet); + const handleStyleChange = (style) => { + setCurrentBrew(prevBrew => ({ ...prevBrew, style })); + if (autoSave) debounceSave(); + }; - this.setState((prevState)=>({ - brew : { ...prevState.brew, snippets: snippet }, - unsavedChanges : true, - htmlErrors : htmlErrors, - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + const handleSnipChange = (snippet)=>{ + //If there are HTML errors, run the validator on every change to give quick feedback + if(HTMLErrors.length) + HTMLErrors = Markdown.validate(snippet); - handleStyleChange : function(style){ - this.setState((prevState)=>({ - brew : { ...prevState.brew, style: style } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setHTMLErrors(HTMLErrors); + setCurrentBrew((prevBrew) => ({ ...prevBrew, snippets: snippet })); + if (autoSave) debounceSave(); + }; - handleMetaChange : function(metadata, field=undefined){ - if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed - fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme); + const handleMetaChange = (metadata, field = undefined) => { + if (field === 'theme' || field === 'renderer') + fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme); - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - ...metadata - } - }), ()=>{if(this.state.autoSave) this.trySave();}); - }, + setCurrentBrew(prev => ({ ...prev, ...metadata })); + if (autoSave) debounceSave(); + }; - hasChanges : function(){ - return !_.isEqual(this.state.brew, this.savedBrew); - }, - - updateBrew : function(newData){ - this.setState((prevState)=>({ - brew : { - ...prevState.brew, - style : newData.style, - text : newData.text, - snippets : newData.snippets - } + const updateBrew = (newData) => + setCurrentBrew((prevBrew) => ({ + ...prevBrew, + style : newData.style, + text : newData.text, + snippets : newData.snippets })); - }, - - trySave : function(immediate=false){ - if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); - if(this.state.isSaving) - return; - - if(immediate) { - this.debounceSave(); - this.debounceSave.flush(); - return; - } - if(this.hasChanges()) - this.debounceSave(); - else - this.debounceSave.cancel(); - }, + const trySave = (immediate = false) => { + if (!debounceSave.current) return; + if (isSaving) return; - handleGoogleClick : function(){ - if(!global.account?.googleId) { - this.setState({ - alertLoginToTransfer : true - }); + const hasChange = !_.isEqual(currentBrew, savedBrew.current); + + if (immediate) { + debounceSave.current(); + debounceSave.current.flush?.(); return; } - this.setState((prevState)=>({ - confirmGoogleTransfer : !prevState.confirmGoogleTransfer - })); - this.setState({ - error : null - }); - }, - closeAlerts : function(event){ - event.stopPropagation(); //Only handle click once so alert doesn't reopen - this.setState({ - alertTrashedGoogleBrew : false, - alertLoginToTransfer : false, - confirmGoogleTransfer : false - }); - }, + if (hasChange) { + debounceSave.current(); + } else { + debounceSave.current.cancel?.(); + } + }; - toggleGoogleStorage : function(){ - this.setState((prevState)=>({ - saveGoogle : !prevState.saveGoogle, - error : null - }), ()=>this.trySave(true)); - }, + const handleGoogleClick = () => { + if (!global.account?.googleId) { + setAlertLoginToTransfer(true); + return; + } - save : async function(){ - if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); + setConfirmGoogleTransfer((prev) => !prev); + setError(null); + }; - const brewState = this.state.brew; // freeze the current state - const preSaveSnapshot = { ...brewState }; + const closeAlerts = (e) => { + e.stopPropagation(); //Only handle click once so alert doesn't reopen + setAlertTrashedGoogleBrew(false); + setAlertLoginToTransfer(false); + setConfirmGoogleTransfer(false); + }; - this.setState((prevState)=>({ - isSaving : true, - error : null, - htmlErrors : Markdown.validate(prevState.brew.text) - })); + const toggleGoogleStorage = () => { + setSaveGoogle((prev) => !prev); + setError(null); + trySave(true); + }; - await updateHistory(this.state.brew).catch(console.error); + const save = async () => { + debounceSave.current?.cancel?.(); + + setIsSaving(true); + setError(null); + setHTMLErrors(Markdown.validate(currentBrew.text)); + + await updateHistory(currentBrew).catch(console.error); await versionHistoryGarbageCollection().catch(console.error); //Prepare content to send to server - const brew = { ...brewState }; - brew.text = brew.text.normalize('NFC'); - this.savedBrew.text = this.savedBrew.text.normalize('NFC'); - brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; - brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text))); - brew.hash = await md5(this.savedBrew.text); - //brew.text = undefined; - Temporary parallel path - brew.textBin = undefined; + const brewToSave = { + ...currentBrew, + text : currentBrew.text.normalize('NFC'), + pageCount: ((currentBrew.renderer === 'legacy' ? currentBrew.text.match(/\\page/g) : currentBrew.text.match(/^\\page$/gm)) || []).length + 1, + patches : stringifyPatches(makePatches(encodeURI(savedBrew.current.text.normalize('NFC')), encodeURI(currentBrew.text.normalize('NFC')))), + hash : await md5(savedBrew.current.text), + textBin : undefined + }; - const compressedBrew = gzipSync(strToU8(JSON.stringify(brew))); + const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave))); + const transfer = saveGoogle === _.isNil(currentBrew.googleId); + const params = transfer ? `?${saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''; - const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId); - const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`; const res = await request - .put(`/api/update/${brew.editId}${params}`) + .put(`/api/update/${brewToSave.editId}${params}`) .set('Content-Encoding', 'gzip') .set('Content-Type', 'application/json') .send(compressedBrew) .catch((err)=>{ - console.log('Error Updating Local Brew'); - this.setState({ error: err }); + console.error('Error Updating Local Brew'); + setError(err); }); if(!res) return; - this.savedBrew = { - ...preSaveSnapshot, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version + const { googleId, editId, shareId, version } = res.body; + + savedBrew.current = { + ...currentBrew, + googleId: googleId ?? null, + editId, + shareId, + version }; - this.setState((prevState) => ({ - brew: { - ...prevState.brew, - googleId : res.body.googleId ? res.body.googleId : null, - editId : res.body.editId, - shareId : res.body.shareId, - version : res.body.version - }, - isSaving : false, - unsavedTime : new Date() - }), ()=>{ - this.setState({ unsavedChanges : this.hasChanges() }); - }); + setCurrentBrew(prev => ({ + ...prev, + googleId: googleId ?? null, + editId, + shareId, + version + })); - history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); - }, + setIsSaving(false); + setUnsavedTime(new Date()); + setUnsavedChanges(!_.isEqual(currentBrew, savedBrew.current)); - renderGoogleDriveIcon : function(){ - return - Google Drive icon + history.replaceState(null, null, `/edit/${editId}`); + }; - {this.state.confirmGoogleTransfer && -
- { this.state.saveGoogle - ? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?` - : `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?` - } + const renderGoogleDriveIcon = () => ( + + Google Drive icon + + {confirmGoogleTransfer && ( +
+ {saveGoogle + ? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?' + : 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
-
- Yes -
-
- No -
+
Yes
+
No
- } + )} - {this.state.alertLoginToTransfer && -
- You must be signed in to a Google account to transfer - between the homebrewery and Google Drive! - -
- Sign In -
+ {alertLoginToTransfer && ( +
+ You must be signed in to a Google account to transfer between the homebrewery and Google Drive! + +
Sign In
-
- Not Now -
+
Not Now
- } + )} - {this.state.alertTrashedGoogleBrew && -
- This brew is currently in your Trash folder on Google Drive!
If you want to keep it, make sure to move it before it is deleted permanently!
-
- OK -
+ {alertTrashedGoogleBrew && ( +
+ This brew is currently in your Trash folder on Google Drive!
+ If you want to keep it, make sure to move it before it is deleted permanently!
+
OK
- } - ; - }, - - renderSaveButton : function(){ + )} + + ); + const renderSaveButton = () => { // #1 - Currently saving, show SAVING - if(this.state.isSaving){ + if (isSaving) return saving...; - } // #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING - if(this.state.unsavedChanges && this.state.autoSaveWarning){ - this.setAutosaveWarning(); - const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60); - const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; + if (unsavedChanges && autoSaveWarning) { + setAutosaveWarning(); + const elapsedTime = Math.round((new Date() - unsavedTime) / 1000 / 60); + const text = elapsedTime === 0 + ? 'Autosave is OFF.' + : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`; return - Reminder... -
- {text} -
-
; + Reminder... +
{text}
+ } // #3 - Unsaved changes exist, click to save, show SAVE NOW - // Use trySave(true) instead of save() to use debounced save function - if(this.state.unsavedChanges){ - return this.trySave(true)} color='blue' icon='fas fa-save'>Save Now; - } + if (unsavedChanges) + return trySave(true)} color='blue' icon='fas fa-save'>Save Now + // #4 - No unsaved changes, autosave is ON, show AUTO-SAVED - if(this.state.autoSave){ + if (autoSave) return auto-saved.; - } + // DEFAULT - No unsaved changes, show SAVED return saved.; - }, + }; - handleAutoSave : function(){ - if(this.warningTimer) clearTimeout(this.warningTimer); - this.setState((prevState)=>({ - autoSave : !prevState.autoSave, - autoSaveWarning : prevState.autoSave - }), ()=>{ - localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave)); - }); - }, + const handleAutoSave = () => { + if (warningTimer.current) clearTimeout(warningTimer.current); + localStorage.setItem('AUTOSAVE_ON', JSON.stringify(!autoSaveEnabled)); + setAutoSave(!autoSave); + setAutoSaveWarning(false); + }; - setAutosaveWarning : function(){ - setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display - this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings - this.warningTimer; - }, + const resetAutosaveWarning = () => { + setTimeout(setAutoSaveWarning(false), 4000); // Hide the warning after 4 seconds + warningTimer.current = setTimeout(setAutoSaveWarning(true), 900000); // 15 minutes between unsaved changes warnings + }; - errorReported : function(error) { - this.setState({ - error - }); - }, + const renderAutoSaveButton = () => ( + + Autosave + + ); - renderAutoSaveButton : function(){ - return - Autosave - ; - }, + const processShareId = () => ( + currentBrew.googleId && !currentBrew.stubbed + ? currentBrew.googleId + currentBrew.shareId + : currentBrew.shareId + ); - processShareId : function() { - return this.state.brew.googleId && !this.state.brew.stubbed ? - this.state.brew.googleId + this.state.brew.shareId : - this.state.brew.shareId; - }, - - getRedditLink : function(){ - - const shareLink = this.processShareId(); - const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : ''; - const title = `${this.props.brew.title} ${systems}`; + const getRedditLink = () => { + const shareLink = processShareId(); + const systems = currentBrew.systems.length > 0 ? ` [${currentBrew.systems.join(' - ')}]` : ''; + const title = `${currentBrew.title} ${systems}`; const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out. -**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; + **[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`; return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`; - }, + }; - clearError : function(){ - setState({ - error : null, - isSaving : false - }) - }, + const clearError = () => { + setError(null); + setIsSaving(false); + }; renderNavbar : function(){ const shareLink = this.processShareId(); @@ -502,7 +428,7 @@ const EditPage = createClass({ onStyleChange={this.handleStyleChange} onSnipChange={this.handleSnipChange} onMetaChange={this.handleMetaChange} - reportError={this.errorReported} + reportError={setError} renderer={this.state.brew.renderer} userThemes={this.props.userThemes} themeBundle={this.state.themeBundle}