Redo all of explore together in React (#617)

* unify explore in react

* no more need for manual scroll reseting

* we're not opening/closing the search anymore
This commit is contained in:
Connor Turland 2016-08-21 21:02:49 -04:00 committed by GitHub
parent c89a6771ea
commit d7759c8c07
15 changed files with 268 additions and 275 deletions

View file

@ -43,5 +43,4 @@
//= require ./src/Metamaps.Mobile
//= require ./src/Metamaps.Admin
//= require ./src/Metamaps.Import
//= require ./src/Metamaps.Header
//= require ./src/Metamaps.JIT

View file

@ -121,36 +121,6 @@ Metamaps.Backbone.Map = Backbone.Model.extend({
}
return this.get('mappers')
},
attrForCards: function () {
function capitalize (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
var n = this.get('name')
var d = this.get('desc')
var maxNameLength = 32
var maxDescLength = 118
var truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : ''
var truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : ''
var obj = {
id: this.id,
name: truncatedName,
fullName: n,
desc: truncatedDesc,
permission: this.get('permission') ? capitalize(this.get('permission')) : 'Commons',
editPermission: this.authorizeToEdit(Metamaps.Active.Mapper) ? 'canEdit' : 'cannotEdit',
contributor_count_number: '<span class="cCountColor">' + this.get('contributor_count') + '</span>',
contributor_count_string: this.get('contributor_count') === 1 ? ' contributor' : ' contributors',
topic_count_number: '<span class="tCountColor">' + this.get('topic_count') + '</span>',
topic_count_string: this.get('topic_count') === 1 ? ' topic' : ' topics',
synapse_count_number: '<span class="sCountColor">' + this.get('synapse_count') + '</span>',
synapse_count_string: this.get('synapse_count') === 1 ? ' synapse' : ' synapses',
screenshot: '<img src="' + this.get('screenshot_url') + '" />'
}
return obj
},
updateView: function () {
var map = Metamaps.Active.Map
var isActiveMap = this.id === map.id

View file

@ -51,31 +51,26 @@ $(document).ready(function () {
Metamaps[prop].hasOwnProperty('init') &&
typeof (Metamaps[prop].init) == 'function'
) {
Metamaps[prop].init();
Metamaps[prop].init()
}
}
// load whichever page you are on
if (Metamaps.currentSection === "explore") {
var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1);
var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1)
Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] );
Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] )
if (Metamaps.currentPage === "mapper") {
Metamaps.Views.exploreMaps.fetchUserThenRender();
Metamaps.Header.fetchUserThenChangeSection(!!Metamaps.Active.Mapper, Metamaps.Maps.Mapper.mapperId)
Metamaps.Views.exploreMaps.fetchUserThenRender()
}
else {
Metamaps.Views.exploreMaps.render();
Metamaps.Header.changeSection(!!Metamaps.Active.Mapper, Metamaps.currentPage)
Metamaps.Views.exploreMaps.render()
}
Metamaps.GlobalUI.showDiv('#exploreMaps')
Metamaps.GlobalUI.showDiv('#exploreMapsHeader')
Metamaps.GlobalUI.showDiv('#explore')
}
else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) {
Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps.Active );
Metamaps.Views.exploreMaps.render();
Metamaps.GlobalUI.showDiv('#exploreMaps')
Metamaps.Header.changeSection(!!Metamaps.Active.Mapper, 'active')
Metamaps.GlobalUI.showDiv('#exploreMapsHeader')
Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active)
Metamaps.Views.exploreMaps.render()
Metamaps.GlobalUI.showDiv('#explore')
}
else if (Metamaps.Active.Map || Metamaps.Active.Topic) {
Metamaps.Loading.show()

View file

@ -1,22 +0,0 @@
/* global Metamaps, $ */
Metamaps.Header = {
init: function () {
},
fetchUserThenChangeSection: function (signedIn, mapperId) {
$.ajax({
url: '/users/' + mapperId + '.json',
success: function (response) {
Metamaps.Header.changeSection(signedIn, 'mapper', response.image, response.name)
},
error: function () {}
});
},
changeSection: function (signedIn, section, userAvatar, userName) {
ReactDOM.render(
React.createElement(Metamaps.ReactComponents.Header, { signedIn: signedIn, section: section, userAvatar: userAvatar, userName: userName }),
document.getElementById('exploreMapsHeader')
);
}
}

View file

@ -46,13 +46,7 @@
if (Metamaps.Active.Mapper) {
Metamaps.GlobalUI.hideDiv('#yield')
Metamaps.Header.changeSection(!!Metamaps.Active.Mapper, 'active')
Metamaps.GlobalUI.showDiv('#exploreMapsHeader')
Metamaps.GlobalUI.showDiv('#exploreMaps')
$('#exploreMaps').scrollTop(0)
Metamaps.GlobalUI.Search.open()
Metamaps.GlobalUI.Search.lock()
Metamaps.GlobalUI.showDiv('#explore')
Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active)
if (Metamaps.Maps.Active.length === 0) {
@ -62,11 +56,8 @@
}
} else {
// logged out home page
Metamaps.GlobalUI.hideDiv('#exploreMapsHeader')
Metamaps.GlobalUI.hideDiv('#exploreMaps')
Metamaps.GlobalUI.hideDiv('#explore')
Metamaps.GlobalUI.showDiv('#yield')
Metamaps.GlobalUI.Search.unlock()
//Metamaps.GlobalUI.Search.close(0, true)
Metamaps.Router.timeoutId = setTimeout(navigate, 500)
}
@ -143,15 +134,7 @@
}
}
Metamaps.GlobalUI.Search.open()
Metamaps.GlobalUI.Search.lock()
Metamaps.GlobalUI.showDiv('#exploreMaps')
Metamaps.GlobalUI.showDiv('#exploreMapsHeader')
$('#exploreMaps').scrollTop(0)
if (id) {
Metamaps.Header.fetchUserThenChangeSection(!!Metamaps.Active.Mapper, id)
}
else Metamaps.Header.changeSection(!!Metamaps.Active.Mapper, section)
Metamaps.GlobalUI.showDiv('#explore')
Metamaps.GlobalUI.hideDiv('#yield')
Metamaps.GlobalUI.hideDiv('#infovis')
Metamaps.GlobalUI.hideDiv('#instructions')
@ -174,8 +157,7 @@
// can edit this map '.canEditMap'
Metamaps.GlobalUI.hideDiv('#yield')
Metamaps.GlobalUI.hideDiv('#exploreMaps')
Metamaps.GlobalUI.hideDiv('#exploreMapsHeader')
Metamaps.GlobalUI.hideDiv('#explore')
// clear the visualization, if there was one, before showing its div again
if (Metamaps.Visualize.mGraph) {
@ -187,9 +169,6 @@
Metamaps.Topic.end()
Metamaps.Active.Topic = null
//Metamaps.GlobalUI.Search.unlock()
//Metamaps.GlobalUI.Search.close(0, true)
Metamaps.Loading.show()
Metamaps.Map.end()
Metamaps.Map.launch(id)
@ -206,8 +185,7 @@
$('.wrapper').addClass('topicPage')
Metamaps.GlobalUI.hideDiv('#yield')
Metamaps.GlobalUI.hideDiv('#exploreMaps')
Metamaps.GlobalUI.hideDiv('#exploreMapsHeader')
Metamaps.GlobalUI.hideDiv('#explore')
// clear the visualization, if there was one, before showing its div again
if (Metamaps.Visualize.mGraph) {
@ -219,9 +197,6 @@
Metamaps.Map.end()
Metamaps.Active.Map = null
//Metamaps.GlobalUI.Search.unlock()
//Metamaps.GlobalUI.Search.close(0, true)
Metamaps.Topic.end()
Metamaps.Topic.launch(id)
}

View file

@ -1,128 +1,87 @@
/* global Metamaps, $, Hogan, Backbone */
/* global Metamaps, $ */
/*
* Metamaps.Views.js.erb
*
* Dependencies:
* - Metamaps.Famous
* - Metamaps.Loading
* - Metamaps.Active
* - Metamaps.ReactComponents
*/
Metamaps.Views = {
initialized: false
}
Metamaps.Views.init = function () {
Metamaps.Views.MapperCard = Backbone.View.extend({
template: Hogan.compile($('#mapperCardTemplate').html()),
tagName: 'div',
className: 'mapper',
render: function () {
this.$el.html(this.template.render(this.model))
return this
}
})
Metamaps.Views.MapCard = Backbone.View.extend({
template: Hogan.compile($('#mapCardTemplate').html()),
tagName: 'div',
className: 'map',
id: function () {
return this.model.id
},
initialize: function () {
this.listenTo(this.model, 'change', this.render)
},
render: function () {
this.$el.html(this.template.render(this.model.attrForCards()))
return this
}
})
var MapsWrapper = Backbone.View.extend({
initialize: function (opts) {},
exploreMaps: {
setCollection: function (collection) {
if (this.collection) this.stopListening(this.collection)
this.collection = collection
this.listenTo(this.collection, 'add', this.render)
this.listenTo(this.collection, 'successOnFetch', this.handleSuccess)
this.listenTo(this.collection, 'errorOnFetch', this.handleError)
var self = Metamaps.Views.exploreMaps
if (self.collection) {
self.collection.off('add', self.render)
self.collection.off('successOnFetch', self.handleSuccess)
self.collection.off('errorOnFetch', self.handleError)
}
self.collection = collection
self.collection.on('add', self.render)
self.collection.on('successOnFetch', self.handleSuccess)
self.collection.on('errorOnFetch', self.handleError)
},
render: function (mapperObj, cb) {
var that = this
var self = Metamaps.Views.exploreMaps
if (typeof mapperObj === 'function') {
cb = mapperObj
mapperObj = null
}
this.el.innerHTML = ''
// in case it is a page where we have to display the mapper card
if (mapperObj) {
var view = new Metamaps.Views.MapperCard({ model: mapperObj })
that.el.appendChild(view.render().el)
}
this.collection.each(function (map) {
var view = new Metamaps.Views.MapCard({ model: map })
that.el.appendChild(view.render().el)
})
this.$el.append('<div class="clearfloat"></div>')
if (this.collection.length >= 20 && this.collection.page != "loadedAll") {
this.$el.append('<button class="button loadMore">load more</button>')
this.$el.append('<div class="clearfloat"></div>')
var exploreObj = {
currentUser: Metamaps.Active.Mapper,
section: self.collection.id,
displayStyle: 'grid',
maps: self.collection,
moreToLoad: self.collection.page != 'loadedAll',
user: mapperObj,
loadMore: self.loadMore
}
ReactDOM.render(
React.createElement(Metamaps.ReactComponents.Maps, exploreObj),
document.getElementById('explore')
)
$('#exploreMaps').empty().html(this.el)
this.$el.find('.loadMore').click(that.loadMore.bind(that))
if (cb) cb()
Metamaps.Loading.hide()
},
loadMore: function () {
if (this.collection.page != "loadedAll") {
this.collection.getMaps();
}
else {
this.$el.find('.loadMore').hide()
var self = Metamaps.Views.exploreMaps
if (self.collection.page != "loadedAll") {
self.collection.getMaps()
}
else self.render()
},
handleSuccess: function (cb) {
if (this.collection && this.collection.id === 'mapper') {
this.fetchUserThenRender(cb)
var self = Metamaps.Views.exploreMaps
if (self.collection && self.collection.id === 'mapper') {
self.fetchUserThenRender(cb)
} else {
this.render(cb)
self.render(cb)
}
},
handleError: function () {
console.log('error loading maps!') // TODO
},
fetchUserThenRender: function (cb) {
var that = this
var self = Metamaps.Views.exploreMaps
// first load the mapper object and then call the render function
$.ajax({
url: '/users/' + this.collection.mapperId + '/details.json',
url: '/users/' + self.collection.mapperId + '/details.json',
success: function (response) {
that.render(response, cb)
self.render(response, cb)
},
error: function () {
that.render(cb)
self.render(cb)
}
})
}
})
Metamaps.Views.exploreMaps = new MapsWrapper()
}
}

View file

@ -590,8 +590,11 @@
/* explore maps */
#exploreMaps {
#explore {
display: none;
}
#exploreMaps {
padding: 0 5%;
position: absolute;
width: 90%;
@ -613,7 +616,6 @@
}
#exploreMapsHeader {
display: none;
position: absolute;
width: 100%;
}

View file

@ -45,55 +45,6 @@
</div>
</div>
</script>
<script type="text/template" id="mapCardTemplate">
<a href="/maps/{{id}}" data-router="true">
<div class="permission {{editPermission}}"> <!-- must be canEdit or cannotEdit -->
<div class="mapCard">
<span class="title" title="{{fullName}}">
{{name}}
</span>
<div class="mapScreenshot">
{{{screenshot}}}
</div>
<div class="scroll">
<div class="desc">
{{desc}}
<div class="clearfloat"></div>
</div>
</div>
<div class="mapMetadata">
<div class="metadataSection">{{{contributor_count_number}}}{{contributor_count_string}}</div>
<div class="metadataSection">{{{topic_count_number}}}{{topic_count_string}}</div>
<div class="metadataSection mapPermission">{{permission}}</div>
<div class="metadataSection">{{{synapse_count_number}}}{{synapse_count_string}}</div>
<div class="clearfloat"></div>
</div>
</div>
</div>
</a>
</script>
<script type="text/template" id="mapperCardTemplate">
<div class="mapperCard">
<div class="mapperImage">
<img src="{{image}}" width="96" height="96" />
</div>
<div class="mapperName" title="{{name}}">
{{name}}
</div>
<div class="mapperInfo">
<div class="mapperCreatedAt">Mapper since: {{created_at}}</div>
<div class="mapperGeneration">Generation: {{generation}}</div>
</div>
<div class="mapperMetadata">
<div class="metadataSection metadataMaps"><div>{{numMaps}}</div>maps</div>
<div class="metadataSection metadataTopics"><div>{{numTopics}}</div>topics</div>
<div class="metadataSection metadataSynapses"><div>{{numSynapses}}</div>synapses</div>
<div class="clearfloat"></div>
</div>
</div>
</script>
<script type="text/template" id="topicSearchTemplate">
<div class="result{{rtype}}">

View file

@ -44,8 +44,7 @@
<% end %>
<%= render :partial => 'layouts/lowermapelements' %>
<div id="exploreMaps"></div>
<div id="exploreMapsHeader"></div>
<div id="explore"></div>
<div id="instructions">
<div class="addTopic">

View file

@ -32,41 +32,43 @@ class Header extends Component {
const mapper = section == "mapper"
return (
<div className="exploreMapsBar exploreElement">
<div className="exploreMapsMenu">
<div className="exploreMapsCenter">
<MapLink show={signedIn && explore}
href="/explore/mine"
linkClass={activeClass("my")}
data-router="true"
text="My Maps"
/>
<MapLink show={signedIn && explore}
href="/explore/shared"
linkClass={activeClass("shared")}
data-router="true"
text="Shared With Me"
/>
<MapLink show={explore}
href={signedIn ? "/" : "/explore/active"}
linkClass={activeClass("active")}
data-router="true"
text="Recently Active"
/>
<MapLink show={!signedIn && explore}
href="/explore/featured"
linkClass={activeClass("featured")}
data-router="true"
text="Featured Maps"
/>
{mapper ? (
<div className='exploreMapsButton active mapperButton'>
<img className='exploreMapperImage' width='24' height='24' src={this.props.userAvatar} />
<div className='exploreMapperName'>{this.props.userName}&rsquo;s Maps</div>
<div className='clearfloat'></div>
</div>
) : null }
<div id="exploreMapsHeader">
<div className="exploreMapsBar exploreElement">
<div className="exploreMapsMenu">
<div className="exploreMapsCenter">
<MapLink show={signedIn && explore}
href="/explore/mine"
linkClass={activeClass("my")}
data-router="true"
text="My Maps"
/>
<MapLink show={signedIn && explore}
href="/explore/shared"
linkClass={activeClass("shared")}
data-router="true"
text="Shared With Me"
/>
<MapLink show={explore}
href={signedIn ? "/" : "/explore/active"}
linkClass={activeClass("active")}
data-router="true"
text="Recently Active"
/>
<MapLink show={!signedIn && explore}
href="/explore/featured"
linkClass={activeClass("featured")}
data-router="true"
text="Featured Maps"
/>
{mapper ? (
<div className='exploreMapsButton active mapperButton'>
<img className='exploreMapperImage' width='24' height='24' src={this.props.user.image} />
<div className='exploreMapperName'>{this.props.user.name}&rsquo;s Maps</div>
<div className='clearfloat'></div>
</div>
) : null }
</div>
</div>
</div>
</div>
@ -77,8 +79,7 @@ class Header extends Component {
Header.propTypes = {
signedIn: PropTypes.bool.isRequired,
section: PropTypes.string.isRequired,
userAvatar: PropTypes.string,
userName: PropTypes.string
user: PropTypes.object
}
export default Header

View file

@ -0,0 +1,74 @@
import React, { Component, PropTypes } from 'react'
class MapCard extends Component {
render = () => {
const { map, currentUser } = this.props
function capitalize (string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
const n = map.get('name')
const d = map.get('desc')
const maxNameLength = 32
const maxDescLength = 118
const truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : ''
const truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : ''
const editPermission = map.authorizeToEdit(currentUser) ? 'canEdit' : 'cannotEdit'
return (
<div className="map" id={ map.id }>
<a href={ '/maps/' + map.id } data-router="true">
<div className={ 'permission ' + editPermission }>
<div className="mapCard">
<span className="title" title={ map.get('name') }>
{ truncatedName }
</span>
<div className="mapScreenshot">
<img src={ map.get('screenshot_url') } />
</div>
<div className="scroll">
<div className="desc">
{ truncatedDesc }
<div className="clearfloat"></div>
</div>
</div>
<div className="mapMetadata">
<div className="metadataSection">
<span className="cCountColor">
{ map.get('contributor_count') }
</span>
{ map.get('contributor_count') === 1 ? ' contributor' : ' contributors' }
</div>
<div className="metadataSection">
<span className="tCountColor">
{ map.get('topic_count') }
</span>
{ map.get('topic_count') === 1 ? ' topic' : ' topics' }
</div>
<div className="metadataSection mapPermission">
{ map.get('permission') ? capitalize(map.get('permission')) : 'Commons' }
</div>
<div className="metadataSection">
<span className="sCountColor">
{ map.get('synapse_count') }
</span>
{ map.get('synapse_count') === 1 ? ' synapse' : ' synapses' }
</div>
<div className="clearfloat"></div>
</div>
</div>
</div>
</a>
</div>
)
}
}
MapCard.propTypes = {
map: PropTypes.object.isRequired,
currentUser: PropTypes.object
}
export default MapCard

View file

View file

@ -0,0 +1,36 @@
import React, { Component, PropTypes } from 'react'
class MapperCard extends Component {
render = () => {
const { user } = this.props
return (
<div className="mapper">
<div className="mapperCard">
<div className="mapperImage">
<img src={ user.image } width="96" height="96" />
</div>
<div className="mapperName" title={ user.name }>
{ user.name }
</div>
<div className="mapperInfo">
<div className="mapperCreatedAt">Mapper since: { user.created_at }</div>
<div className="mapperGeneration">Generation: { user.generation }</div>
</div>
<div className="mapperMetadata">
<div className="metadataSection metadataMaps"><div>{ user.numMaps }</div>maps</div>
<div className="metadataSection metadataTopics"><div>{ user.numTopics }</div>topics</div>
<div className="metadataSection metadataSynapses"><div>{ user.numSynapses }</div>synapses</div>
<div className="clearfloat"></div>
</div>
</div>
</div>
)
}
}
MapperCard.propTypes = {
user: PropTypes.object.isRequired
}
export default MapperCard

View file

@ -0,0 +1,54 @@
import React, { Component, PropTypes } from 'react'
import Header from './Header.js'
import MapperCard from './MapperCard.js'
import MapCard from './MapCard.js'
import MapListItem from './MapListItem.js'
class Maps extends Component {
render = () => {
const { maps, currentUser, section, displayStyle, user, moreToLoad, loadMore } = this.props
let mapElements
if (displayStyle == 'grid') {
mapElements = maps.models.map(function (map) {
return <MapCard key={ map.id } map={ map } currentUser={ currentUser } />
})
}
else if (displayStyle == 'list') {
mapElements = maps.models.map(function (map) {
return <MapListItem key={ map.id } map={ map } />
})
}
return (
<div>
<div id='exploreMaps'>
<div>
{ user ? <MapperCard user={ user } /> : null }
{ mapElements }
<div className='clearfloat'></div>
{ moreToLoad ?
[<button className="button loadMore" onClick={ loadMore }>load more</button>, <div className='clearfloat'></div>]
: null }
</div>
</div>
<Header signedIn={ !!currentUser }
section={ section }
user={ user }
/>
</div>
)
}
}
Maps.propTypes = {
section: PropTypes.string.isRequired,
maps: PropTypes.object.isRequired,
moreToLoad: PropTypes.bool.isRequired,
displayStyle: PropTypes.string,
user: PropTypes.object,
currentUser: PropTypes.object,
loadMore: PropTypes.func
}
export default Maps

View file

@ -2,7 +2,7 @@ import React from 'react'
import ReactDOM from 'react-dom'
import Backbone from 'backbone'
import _ from 'underscore'
import Header from './components/Header.js'
import Maps from './components/Maps.js'
// this is optional really, if we import components directly React will be
// in the bundle, so we won't need a global reference
@ -14,5 +14,5 @@ window._ = _
window.Metamaps = window.Metamaps || {}
window.Metamaps.ReactComponents = {
Header
Maps
}