diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index 0c7b9976..52b0fc29 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -193,10 +193,6 @@ button.button.btn-no:hover { display: block; width: 830px; } -.requestInvite { - display: block; - margin: -720px auto 0; -} .new_session, .new_user, .edit_user, @@ -672,9 +668,21 @@ label { position: relative; /*overflow:hidden; */ } -.main.compressed { - width: calc(100% - 300px); +.compressed { + .upperRightUI { + right: 324px; + } + .upperRightMapButtons { + right: 434px; + } + .mapControls { + right: 324px; + } + .infoAndHelp { + right: 370px; + } } + #infovis-canvas { -webkit-touch-callout: none; -webkit-user-select: none; @@ -775,9 +783,9 @@ label { } .sidebarAccountIcon img { border-radius: 16px; + width: 32px; } .sidebarAccountBox { - display: none; height: auto; } .authenticated .sidebarAccountBox { @@ -1039,7 +1047,6 @@ label[for="user_remember_me"] { } .sidebarFilterBox { - display:none; width: 319px; padding: 16px 0; overflow-y: auto; @@ -3058,14 +3065,16 @@ and it won't be important on password protected instances */ /* request */ -#wrapper .requestInvite { +.requestInvite { width: 700px; - margin: 0 auto; - padding: 0 0 60px 0; background: #FFFFFF; color: white; - height: 100%; - overflow: hidden; + height: calc(100% - 52px); + z-index: 1; + position: relative; + left: 50%; + margin-left: -350px; + margin-top: 52px; } .home_bg { @@ -3148,4 +3157,4 @@ script.data-gratipay-username { background: #FFF; cursor: pointer; font-family: din-regular; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/base.scss.erb b/app/assets/stylesheets/base.scss.erb index 355f5652..f4e247f2 100644 --- a/app/assets/stylesheets/base.scss.erb +++ b/app/assets/stylesheets/base.scss.erb @@ -54,7 +54,6 @@ width:100%; height:100%; position: absolute; - display: none; } .showcard .permission { @@ -233,7 +232,7 @@ background-repeat:no-repeat; } } - + .contributor { bottom: 7px; margin-left: 40px; diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 1582404e..e1c56d33 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -30,6 +30,7 @@ height: 100%; box-sizing: border-box; padding-top: 92px; + overflow-y: auto; } /*.animations { @@ -46,26 +47,9 @@ transition-timing-function: ease-in-out; }*/ -.mapElement { - display: none; -} -.mapPage .mapElement, -.topicPage .mapElement { - display: block; -} -.mapPage .mapElementHidden, -.topicPage .mapElement.mapInfoBox, -.topicPage .mapElement.importDialog { - display:none; -} -.topicPage .starMap { - display: none; -} - /* loading */ #loading { - display: none; width: 28px; height: 28px; position: fixed; @@ -184,10 +168,10 @@ } .upperRightMapButtons { - top: -42px; /* puts it just offscreen */ + right: 134px; } -.mapPage .upperRightMapButtons, .topicPage .upperRightMapButtons { - top: 0; +.unauthenticated .upperRightMapButtons { + right: 115px; } .upperRightIcon { @@ -197,13 +181,7 @@ background-repeat: no-repeat; cursor: pointer; } -.mapPage .mapElement .importDialog { - display: none; - background-position: 0 0; -} -.mapPage.canEditMap .mapElement .importDialog { - display: block; -} + .sidebarFilterIcon { background-position: -32px 0; } @@ -236,6 +214,14 @@ /* end upperRightUI */ +/* map wrapper */ +.mapWrapper { + position:absolute; + width: 100%; + height: 100%; +} + +/* end map wrapper */ /* yield */ @@ -356,22 +342,15 @@ /* infoAndHelp */ -.mapPage .infoAndHelp, .topicPage .infoAndHelp { - right: 70px; -} -.mapPage .openCheatsheet .tooltipsAbove, .topicPage .openCheatsheet .tooltipsAbove { +.openCheatsheet .tooltipsAbove { right: 1px; left: auto; } -.unauthenticated .homePage .infoAndHelp { - display:none; -} - .infoAndHelp { position: absolute; bottom: 20px; - right: 20px; + right: 70px; z-index: 3; width: auto; font-style: italic; @@ -392,16 +371,12 @@ } .mapInfoIcon { position: relative; - top: 56px; /* puts it just offscreen */ - background-image: url(<%= asset_path('mapinfo_sprite.png') %>); - background-repeat:no-repeat; + background-image: url(<%= asset_path('mapinfo_sprite.png') %>); + background-repeat:no-repeat; } .mapInfoIcon:hover { background-position: 0 -32px; } -.mapPage .mapInfoIcon { - top: 0; -} .starMap { background-image: url(<%= asset_path('starmap_sprite.png') %>); @@ -419,9 +394,6 @@ background-position: 0 0; } -.unauthenticated .mapPage .starMap { - display: none; -} /* end infoAndHelp */ @@ -430,24 +402,17 @@ .mapControls { position: absolute; bottom: 24px; - right:-32px; /* puts it just offscreen */ + right:24px; width:32px; z-index: 3; } -.mapPage .mapControls, .topicPage .mapControls { - right: 24px; -} - -.topicPage .zoomExtents { - display: none; -} .mapControl { width:32px; height:32px; background-color: #424242; - background-repeat: no-repeat; - background-position: 0 0; + background-repeat: no-repeat; + background-position: 0 0; cursor:pointer; } @@ -587,10 +552,6 @@ left: -8px; } -.openCheatsheet .tooltipsAbove { - left: -4px; -} - .sidebarAccountIcon .tooltipsUnder { margin-left: -12px; margin-top: 40px; @@ -671,8 +632,11 @@ /* explore maps */ -#explore { - display: none; +#react-app { + position: absolute; + height: 100%; + width: 100%; + overflow-y: auto; } #exploreMaps { @@ -691,8 +655,13 @@ display: block; } -.appsPage #exploreMapsHeader { - display: block; +.requestInviteHeader { + position: absolute; + width: 100%; + z-index:2; + background-color:#FAFAFA; + height: 52px; + box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); } #exploreMapsHeader { @@ -829,7 +798,6 @@ height: 80px; font-family: 'din-regular', helvetica, sans-serif; font-size: 32px; - display: none; text-align: center; color: #999999; z-index: 0; @@ -845,7 +813,6 @@ /* toast */ .toast { - display: none; position: fixed; bottom: 20px; left: 20px; diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index fc34168d..7bf57d63 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -1,7 +1,3 @@ -#mobile_header { - display: none; -} - @media only screen and (max-width : 752px) and (min-width : 504px) { .sidebarSearch .tt-hint, .sidebarSearch .sidebarSearchField { width: 160px !important; @@ -51,10 +47,6 @@ display: none; } - #mobile_header { - display: block; - } - .homeWrapper { width: 96%; padding: 0 2%; @@ -72,7 +64,7 @@ height: auto; } .homeVideo { - width: 100%; + width: 100% !important; height: auto; } .fullWidthWrapper.withPartners { @@ -108,15 +100,23 @@ max-width: 360px; } - #wrapper .requestInvite { + .requestInviteHeader { + display: none; + } + .requestInvite { width: 100%; - padding: 0; + height: calc(100% - 50px); + z-index: 1; + position: relative; + left: 0; + margin-left: 0px; + margin-top: 50px; } #exploreMaps > div { margin-top: 70px; } - + .mapper { width: 100%; margin: 0 0 30px 0; @@ -217,6 +217,7 @@ width: 100%; box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); position: fixed; + z-index: 1; } #menu_icon { @@ -249,7 +250,6 @@ } #mobile_menu { - display: none; background: #EEE; position: fixed; top: 50px; @@ -257,6 +257,7 @@ padding: 10px; width: 200px; box-shadow: 3px 3px 3px rgba(0,0,0,0.23), 3px 3px 3px rgba(0,0,0,0.16); + z-index: 2; li { padding: 10px; @@ -274,16 +275,6 @@ } } -/* - * the mobile menu, even if it's been opened by a user, should - * not show up if they resize their browser back to full size - */ -@media only screen and (max-width : 504px) { - #mobile_menu.visible { - display: block; - } -} - li.mobileMenuUser { border-bottom: 1px solid #BBB; } diff --git a/app/assets/stylesheets/request_access.scss.erb b/app/assets/stylesheets/request_access.scss.erb index 19ae2792..98f1a380 100644 --- a/app/assets/stylesheets/request_access.scss.erb +++ b/app/assets/stylesheets/request_access.scss.erb @@ -1,7 +1,6 @@ .viewOnly { float: left; margin-left: 16px; - display: none; height: 32px; border: 1px solid #BDBDBD; border-radius: 2px; @@ -23,7 +22,7 @@ } .requestNotice { - display: none; + display: inline-block; padding: 0 8px; } @@ -42,16 +41,6 @@ .requestNotAccepted { background-color: #c04f4f; } - - &.sendRequest .requestAccess { - display: inline-block; - } - &.sentRequest .requestPending { - display: inline-block; - } - &.requestDenied .requestNotAccepted { - display: inline-block; - } } .request_access { diff --git a/app/models/message.rb b/app/models/message.rb index 203e8adb..01cd4c34 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -7,7 +7,7 @@ class Message < ApplicationRecord after_create :after_created #after_create :after_created_async - + def user_image user.image.url @@ -21,7 +21,7 @@ class Message < ApplicationRecord def after_created ActionCable.server.broadcast 'map_' + resource.id.to_s, type: 'messageCreated', message: as_json end - + def after_created_async FollowService.follow(resource, user, 'commented') NotificationService.notify_followers(resource, 'map_message', self) diff --git a/app/views/explore/mine.html.erb b/app/views/explore/mine.html.erb index 21f034c3..21295d4f 100644 --- a/app/views/explore/mine.html.erb +++ b/app/views/explore/mine.html.erb @@ -5,7 +5,7 @@ # %> <%= render :partial => 'layouts/lightboxes' %> <%= render :partial => 'layouts/templates' %> <%= render :partial => 'shared/metacodeBgColors' %> - <%= render :partial => 'layouts/googleanalytics' if ENV["GA_TRACKING_CODE"].present? %> diff --git a/app/views/layouts/_googleanalytics.html.erb b/app/views/layouts/_googleanalytics.html.erb index 2acb9501..493e6288 100644 --- a/app/views/layouts/_googleanalytics.html.erb +++ b/app/views/layouts/_googleanalytics.html.erb @@ -10,6 +10,5 @@ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); ga('create', '<%= ENV["GA_TRACKING_CODE"] %>', 'auto'); - ga('send', 'pageview'); diff --git a/app/views/layouts/_lowermapelements.html.erb b/app/views/layouts/_lowermapelements.html.erb deleted file mode 100644 index e3d5aeaf..00000000 --- a/app/views/layouts/_lowermapelements.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
-
Center View
-
Zoom In
-
Zoom Out
-
- -
- <%= render :partial => 'maps/mapinfobox' %> - - <% starred = current_user && @map && current_user.starred_map?(@map) - starClass = starred ? 'starred' : '' - tooltip = starred ? 'Star' : 'Unstar' %> -
<%= tooltip %>
-
Map Info
-
Help
-
-
diff --git a/app/views/layouts/_mobilemenu.html.erb b/app/views/layouts/_mobilemenu.html.erb deleted file mode 100644 index 5ef3a66d..00000000 --- a/app/views/layouts/_mobilemenu.html.erb +++ /dev/null @@ -1,67 +0,0 @@ -
-
- <%= yield(:mobile_title) %> -
- -
-
- -
diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb deleted file mode 100644 index 7ef76bad..00000000 --- a/app/views/layouts/_upperelements.html.erb +++ /dev/null @@ -1,107 +0,0 @@ - - - -
- -
- <%= 'data-router=true' %><% end %>>METAMAPS -
- - -
- -
-
-
-
- - <% request = current_user && @map && @allrequests.find{|a| a.user == current_user} - className = (@map and not policy(@map).update?) ? 'isViewOnly ' : '' - if @map - className += 'sendRequest' if not request - className += 'sentRequest' if request and not request.answered - className += 'requestDenied' if request and request.answered and not request.approved - end %> - -
-
View Only
- <% if current_user %> -
Request Access
-
Request Pending
-
Request Not Accepted
- <% end %> -
-
-
- -
-
- <% if current_user %> -
-
- Import Data -
-
- <% end %> - - -
-
Filter
-
- <%= render :partial => 'shared/filterBox' %> -
-
- - <% if current_user %> - -
-
Save To New Map
-
- <% end %> - -
-
- - <% if current_user %> - - -
- Create New Map -
-
- <% end %> - - - <% if current_user.present? %> - - <%= link_to notifications_path, class: "notificationsIcon upperRightEl upperRightIcon #{user_unread_notification_count > 0 ? 'unread' : 'read'}" do %> -
- Notifications -
- <% if user_unread_notification_count > 0 %> -
- <% end %> - <% end %> -
- <% end %> - - - <% if !(controller_name == "sessions" && action_name == "new") %> -
-
Account
- <% if current_user && current_user.image %> - <%= image_tag current_user.image.url(:thirtytwo), :size => "32x32" %> - <% elsif !current_user %> - SIGN IN -
- <% end %> -
-
- <%= render :partial => 'layouts/account' %> -
-
- <% end %> -
-
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index daa16d02..3ebbbad6 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,83 +6,18 @@ #%> <%= render :partial => 'layouts/head' %> - controller-<%= controller_name %> action-<%= action_name %>"> - -
- - - - <%= content_tag :div, class: "main" do %> - - <% classes = action_name == "home" ? "homePage" : "" - classes += action_name == "home" && authenticated? ? " explorePage" : "" - classes += controller_name == "maps" && action_name == "index" ? " explorePage" : "" - if controller_name == "maps" && action_name == "show" - classes += " mapPage" - if policy(@map).update? - classes += " canEditMap" - end - if @map.permission == "commons" - classes += " commonsMap" - end - end - classes += controller_name == "topics" && action_name == "show" ? " topicPage" : "" - %> - -
- - <%= render :partial => 'layouts/upperelements', :locals => { :noHardHomeLink => controller_name == "notifications" ? true : false } %> - - <%= yield %> - -
- <% if authenticated? %> - <% # for creating and pulling in topics and synapses %> - <% if controller_name == 'maps' && action_name == "conversation" %> - <%= render :partial => 'maps/newtopicsecret' %> - <% else %> - <%= render :partial => 'maps/newtopic' %> - <% end %> - <%= render :partial => 'maps/newsynapse' %> - <% # for populating the change metacode list on the topic card %> - <%= render :partial => 'shared/metacodeoptions' %> - <% end %> - <%= render :partial => 'layouts/lowermapelements' %> - -
- - <% if !(controller_name == 'maps' && action_name == "conversation") %> -
-
- Double-click to
add a topic -
-
- Use Tab & Shift+Tab to select a metacode -
-
- Press Enter to add the topic -
-
- <% end %> - -
- <%= render :partial => 'layouts/mobilemenu' %> - -

- <% if devise_error_messages? %> - <%= devise_error_messages! %> - <% end %> - <% if notice %> - <%= notice %> - <% end %> - <% if alert %> - <%= alert %> - <% end %> -

-
-
- +
+ <%= yield %> + <% if authenticated? %> + <% # for creating and pulling in topics and synapses %> + <% if controller_name == 'maps' && action_name == "conversation" %> + <%= render :partial => 'maps/newtopicsecret' %> + <% else %> + <%= render :partial => 'maps/newtopic' %> + <% end %> + <%= render :partial => 'maps/newsynapse' %> + <% # for populating the change metacode list on the topic card %> + <%= render :partial => 'shared/metacodeoptions' %> <% end %> - <%= render :partial => 'layouts/foot' %> diff --git a/app/views/layouts/doorkeeper.html.erb b/app/views/layouts/doorkeeper.html.erb index f4696a37..b9ff5bef 100644 --- a/app/views/layouts/doorkeeper.html.erb +++ b/app/views/layouts/doorkeeper.html.erb @@ -6,55 +6,26 @@ #%> <%= render :partial => 'layouts/head' %> - - - - - <%= content_tag :div, class: "main" do %> - - <% if params[:controller] == 'doorkeeper/applications' || params[:controller] == 'doorkeeper/authorized_applications' - classes = 'appsPage' - else - classes = '' - end - %> - -
- - <%= render :partial => 'layouts/upperelements', :locals => {:noHardHomeLink => true } %> - - <%= yield %> - -
-
-
-
- <% if current_user && current_user.admin %> - -
Registered Apps -
- <% end %> - -
Authorized Apps -
- -
Maps -
-
-
-
-
-

- <% if devise_error_messages? %> - <%= devise_error_messages! %> - <% elsif notice %> - <%= notice %> - <% end %> -

-
-
- - <% end %> - +
+ <%= yield %> +
+
+
+
+ <% if current_user && current_user.admin %> + +
Registered Apps +
+ <% end %> + +
Authorized Apps +
+ +
Maps +
+
+
+
+
<%= render :partial => 'layouts/foot' %> diff --git a/app/views/main/requestinvite.html.erb b/app/views/main/requestinvite.html.erb index 88a4977c..c026911a 100644 --- a/app/views/main/requestinvite.html.erb +++ b/app/views/main/requestinvite.html.erb @@ -6,7 +6,5 @@ <% content_for :title, "Request Invite | Metamaps" %> <% content_for :mobile_title, "Request Invite" %> - -
+
-
diff --git a/app/views/maps/conversation.html.erb b/app/views/maps/conversation.html.erb index 4ecbd274..51d68a7a 100644 --- a/app/views/maps/conversation.html.erb +++ b/app/views/maps/conversation.html.erb @@ -18,5 +18,6 @@ Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>; Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>; Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>; + Metamaps.ServerData.requests = <%= @allrequests.to_json.html_safe %>; Metamaps.ServerData.VisualizeType = "ForceDirected"; diff --git a/app/views/maps/show.html.erb b/app/views/maps/show.html.erb index 4ecbd274..51d68a7a 100644 --- a/app/views/maps/show.html.erb +++ b/app/views/maps/show.html.erb @@ -18,5 +18,6 @@ Metamaps.ServerData.Mappings = <%= @allmappings.to_json.html_safe %>; Metamaps.ServerData.Messages = <%= @allmessages.to_json.html_safe %>; Metamaps.ServerData.Stars = <%= @allstars.to_json.html_safe %>; + Metamaps.ServerData.requests = <%= @allrequests.to_json.html_safe %>; Metamaps.ServerData.VisualizeType = "ForceDirected"; diff --git a/app/views/shared/_filterBox.html.erb b/app/views/shared/_filterBox.html.erb deleted file mode 100644 index 9ea4926b..00000000 --- a/app/views/shared/_filterBox.html.erb +++ /dev/null @@ -1,125 +0,0 @@ -<%# - # @file - # this code generates the list of icons in the filter box in the upper right menu area - #%> - -<% - @mappers = [] - @synapses = [] - @metacodes = [] - @metacodelist = '' - @mapperlist = '' - @synapselist = '' -# There are essentially three functions happening here one to fill data to -#@mappers with all people who have mapped on the selected map, which -#actually gets checked twice once for topics or within @metacodes and once -#for synapses on the map. @synapses get filled with all synapses on the map -#and metacodes is filled with all the metacodes that are being used on the map. - - if @map - @alltopics.each_with_index do |topic, index| - if @metacodes.index(topic.metacode) == nil - @metacodes.push(topic.metacode) - end - end - @allsynapses.each_with_index do |synapse, index| - if @synapses.index{|s| s.desc == synapse.desc} == nil - @synapses.push(synapse) - end - end - @allmappings.each_with_index do |mapping, index| - if @mappers.index(mapping.user) == nil - @mappers.push(mapping.user) - end - end - elsif @topic - @alltopics.each_with_index do |topic, index| - if @metacodes.index(topic.metacode) == nil - @metacodes.push(topic.metacode) - end - if @mappers.index(topic.user) == nil - @mappers.push(topic.user) - end - end - @allsynapses.each_with_index do |synapse, index| - if @synapses.index{|s| s.desc == synapse.desc} == nil - @synapses.push(synapse) - end - if @mappers.index(synapse.user) == nil - @mappers.push(synapse.user) - end - end - end - - if @map || @topic - @metacodes.sort! {|x,y| - n1 = x.name || "" - n2 = y.name || "" - n1 <=> n2 - } - @synapses.sort! {|x,y| - d1 = x.desc || "" - d2 = y.desc || "" - d1 <=> d2 - } - @mappers.sort! {|x,y| - n1 = x.name || "" - n2 = y.name || "" - n1 <=> n2 - } - - @metacodes.each_with_index do |metacode, index| - @metacodelist += '
  • ' - @metacodelist += '' + metacode.name + '' - @metacodelist += '

    ' + metacode.name.downcase + '

  • ' - end - @synapses.each_with_index do |synapse, index| - d = synapse.desc || "" - @synapselist += '
  • ' - @synapselist += 'synapse icon

    ' + d - @synapselist += '

  • ' - end - @mappers.each_with_index do |mapper, index| - @mapperlist += '
  • ' - @mapperlist += '' + mapper.name + '' - @mapperlist += '

    ' + mapper.name + '

  • ' - end - end -%> -
    -

    FILTER BY

    -
    -

    <%= @map ? "MAPPERS" : @topic ? "CREATORS" : "" %>

    - NONE - ALL -
    - -
    -
    - -
    -

    METACODES

    - NONE - ALL -
    - -
    -
    - -
    -

    SYNAPSES

    - NONE - ALL -
    - -
    -
    - -
    - diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 81b4b39a..6be7c951 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -76,7 +76,7 @@ const Control = { } if (DataModel.Topics.length === 0) { - GlobalUI.showDiv('#instructions') + Map.setHasLearnedTopicCreation(false) } }, deleteSelectedNodes: function() { // refers to deleting topics permanently diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index b01642c5..454ab331 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -1,6 +1,7 @@ /* global $, Hogan, Bloodhound */ import DataModel from './DataModel' +import Map from './Map' import Mouse from './Mouse' import Selected from './Selected' import Synapse from './Synapse' @@ -270,7 +271,7 @@ const Create = { }) Create.newTopic.beingCreated = true Create.newTopic.name = '' - GlobalUI.hideDiv('#instructions') + Map.setHasLearnedTopicCreation(true) }, hide: function(force) { if (force || !Create.newTopic.pinned) { @@ -281,7 +282,7 @@ const Create = { Create.newTopic.pinned = false } if (DataModel.Topics.length === 0) { - GlobalUI.showDiv('#instructions') + Map.setHasLearnedTopicCreation(false) } Create.newTopic.beingCreated = false }, diff --git a/frontend/src/Metamaps/DataModel/Mapper.js b/frontend/src/Metamaps/DataModel/Mapper.js index 20eb5e5a..735bda86 100644 --- a/frontend/src/Metamaps/DataModel/Mapper.js +++ b/frontend/src/Metamaps/DataModel/Mapper.js @@ -9,12 +9,12 @@ const Mapper = Backbone.Model.extend({ toJSON: function(options) { return _.omit(this.attributes, this.blacklist) }, - prepareLiForFilter: function() { - return outdent` -
  • - ${this.get('name')} -

    ${this.get('name')}

    -
  • ` + prepareDataForFilter: function() { + return { + id: this.id, + image: this.get('image'), + name: this.get('name') + } }, followMap: function(id) { const idIndex = this.get('follows').maps.indexOf(id) diff --git a/frontend/src/Metamaps/DataModel/Metacode.js b/frontend/src/Metamaps/DataModel/Metacode.js index e7ee5a31..e75ff3ef 100644 --- a/frontend/src/Metamaps/DataModel/Metacode.js +++ b/frontend/src/Metamaps/DataModel/Metacode.js @@ -9,12 +9,12 @@ const Metacode = Backbone.Model.extend({ image.src = this.get('icon') this.set('image', image) }, - prepareLiForFilter: function() { - return outdent` -
  • - ${this.get('name')} -

    ${this.get('name').toLowerCase()}

    -
  • ` + prepareDataForFilter: function() { + return { + id: this.id, + name: this.get('name'), + icon: this.get('icon') + } } }) diff --git a/frontend/src/Metamaps/DataModel/Synapse.js b/frontend/src/Metamaps/DataModel/Synapse.js index e5cb0ae8..d090d1ba 100644 --- a/frontend/src/Metamaps/DataModel/Synapse.js +++ b/frontend/src/Metamaps/DataModel/Synapse.js @@ -28,12 +28,11 @@ const Synapse = Backbone.Model.extend({ this.on('change', this.updateEdgeView) this.on('change:desc', Filter.checkSynapses, this) }, - prepareLiForFilter: function() { - return outdent` -
  • - synapse icon -

    ${this.get('desc')}

    -
  • ` + prepareDataForFilter: function() { + return { + desc: this.get('desc'), + icon: DataModel.synapseIconUrl + } }, authorizeToEdit: function(mapper) { if (mapper && (this.get('permission') === 'commons' || this.get('collaborator_ids').includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) return true diff --git a/frontend/src/Metamaps/DataModel/index.js b/frontend/src/Metamaps/DataModel/index.js index 272f1717..ce6c975d 100644 --- a/frontend/src/Metamaps/DataModel/index.js +++ b/frontend/src/Metamaps/DataModel/index.js @@ -101,16 +101,19 @@ const DataModel = { }, attachCollectionEvents: function() { DataModel.Topics.on('add remove', function(topic) { + console.log('updating infobox and filters due to topic add or remove') InfoBox.updateNumbers() Filter.checkMetacodes() Filter.checkMappers() }) DataModel.Synapses.on('add remove', function(synapse) { + console.log('updating infobox and filters due to synapse add or remove') InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMappers() }) DataModel.Mappings.on('add remove', function(mapping) { + console.log('updating infobox and filters due to mapping add or remove') InfoBox.updateNumbers() Filter.checkSynapses() Filter.checkMetacodes() diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index 8f45423e..360867b2 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -5,13 +5,17 @@ import _ from 'lodash' import Active from './Active' import Control from './Control' import DataModel from './DataModel' -import GlobalUI from './GlobalUI' +import GlobalUI, { ReactApp } from './GlobalUI' import Settings from './Settings' import Visualize from './Visualize' const Filter = { + dataForPresentation: { + metacodes: {}, + mappers: {}, + synapses: {} + }, filters: { - name: '', metacodes: [], mappers: [], synapses: [] @@ -23,119 +27,26 @@ const Filter = { }, isOpen: false, changing: false, - init: function() { - var self = Filter - - $('.sidebarFilterIcon').click(self.toggleBox) - - $('.sidebarFilterBox .showAllMetacodes').click(self.filterNoMetacodes) - $('.sidebarFilterBox .showAllSynapses').click(self.filterNoSynapses) - $('.sidebarFilterBox .showAllMappers').click(self.filterNoMappers) - $('.sidebarFilterBox .hideAllMetacodes').click(self.filterAllMetacodes) - $('.sidebarFilterBox .hideAllSynapses').click(self.filterAllSynapses) - $('.sidebarFilterBox .hideAllMappers').click(self.filterAllMappers) - - self.bindLiClicks() - self.getFilterData() - }, - toggleBox: function(event) { - var self = Filter - - if (self.isOpen) self.close() - else self.open() - - event.stopPropagation() - }, - open: function() { - var self = Filter - - GlobalUI.Account.close() - $('.sidebarFilterIcon div').addClass('hide') - - if (!self.isOpen && !self.changing) { - self.changing = true - - var height = $(document).height() - 108 - $('.sidebarFilterBox').css('max-height', height + 'px').fadeIn(200, function() { - self.changing = false - self.isOpen = true - }) - } - }, - close: function() { - var self = Filter - $('.sidebarFilterIcon div').removeClass('hide') - - if (!self.changing) { - self.changing = true - - $('.sidebarFilterBox').fadeOut(200, function() { - self.changing = false - self.isOpen = false - }) - } - }, reset: function() { var self = Filter - self.filters.metacodes = [] self.filters.mappers = [] self.filters.synapses = [] self.visible.metacodes = [] self.visible.mappers = [] self.visible.synapses = [] - - $('#filter_by_metacode ul').empty() - $('#filter_by_mapper ul').empty() - $('#filter_by_synapse ul').empty() - - $('.filterBox .showAll').addClass('active') - }, - /* - Most of this data essentially depends on the ruby function which are happening for filter inside view filterBox - But what these function do is load this data into three accessible array within java : metacodes, mappers and synapses - */ - getFilterData: function() { - var self = Filter - - var metacode, mapper, synapse - - $('#filter_by_metacode li').each(function() { - metacode = $(this).attr('data-id') - self.filters.metacodes.push(metacode) - self.visible.metacodes.push(metacode) - }) - - $('#filter_by_mapper li').each(function() { - mapper = ($(this).attr('data-id')) - self.filters.mappers.push(mapper) - self.visible.mappers.push(mapper) - }) - - $('#filter_by_synapse li').each(function() { - synapse = ($(this).attr('data-id')) - self.filters.synapses.push(synapse) - self.visible.synapses.push(synapse) - }) - }, - bindLiClicks: function() { - var self = Filter - $('#filter_by_metacode ul li').unbind().click(self.toggleMetacode) - $('#filter_by_mapper ul li').unbind().click(self.toggleMapper) - $('#filter_by_synapse ul li').unbind().click(self.toggleSynapse) + self.dataForPresentation.metacodes = {} + self.dataForPresentation.mappers = {} + self.dataForPresentation.synapses = {} + ReactApp.render() }, // an abstraction function for checkMetacodes, checkMappers, checkSynapses to reduce // code redundancy - /* - @param - */ updateFilters: function(collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { var self = Filter - var newList = [] var removed = [] var added = [] - // the first option enables us to accept // ['Topics', 'Synapses'] as 'collection' if (typeof collection === 'object') { @@ -168,41 +79,24 @@ const Filter = { } }) } - removed = _.difference(self.filters[filtersToUse], newList) added = _.difference(newList, self.filters[filtersToUse]) - - // remove the list items for things no longer present on the map _.each(removed, function(identifier) { - $('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function() { - $(this).remove() - }) const index = self.visible[filtersToUse].indexOf(identifier) self.visible[filtersToUse].splice(index, 1) + delete self.dataForPresentation[filtersToUse][identifier] }) - - var model, li, jQueryLi - function sortAlpha(a, b) { - return a.childNodes[1].innerHTML.toLowerCase() > b.childNodes[1].innerHTML.toLowerCase() ? 1 : -1 - } - // for each new filter to be added, create a list item for it and fade it in _.each(added, function(identifier) { - model = DataModel[correlatedModel].get(identifier) || - DataModel[correlatedModel].find(function(model) { - return model.get(propertyToCheck) === identifier + const model = DataModel[correlatedModel].get(identifier) || + DataModel[correlatedModel].find(function(m) { + return m.get(propertyToCheck) === identifier }) - li = model.prepareLiForFilter() - jQueryLi = $(li).hide() - $('li', '#filter_by_' + listToModify + ' ul').add(jQueryLi.fadeIn('fast')) - .sort(sortAlpha).appendTo('#filter_by_' + listToModify + ' ul') + self.dataForPresentation[filtersToUse][identifier] = model.prepareDataForFilter() self.visible[filtersToUse].push(identifier) }) - // update the list of filters with the new list we just generated self.filters[filtersToUse] = newList - - // make sure clicks on list items still trigger the right events - self.bindLiClicks() + ReactApp.render() }, checkMetacodes: function() { var self = Filter @@ -221,114 +115,49 @@ const Filter = { var self = Filter self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse') }, - filterAllMetacodes: function(e) { + filterAllMetacodes: function(toVisible) { var self = Filter - $('#filter_by_metacode ul li').addClass('toggledOff') - $('.showAllMetacodes').removeClass('active') - $('.hideAllMetacodes').addClass('active') - self.visible.metacodes = [] + self.visible.metacodes = toVisible ? self.filters.metacodes.slice() : [] + ReactApp.render() self.passFilters() }, - filterNoMetacodes: function(e) { + filterAllMappers: function(toVisible) { var self = Filter - $('#filter_by_metacode ul li').removeClass('toggledOff') - $('.showAllMetacodes').addClass('active') - $('.hideAllMetacodes').removeClass('active') - self.visible.metacodes = self.filters.metacodes.slice() + self.visible.mappers = toVisible ? self.filters.mappers.slice() : [] + ReactApp.render() self.passFilters() }, - filterAllMappers: function(e) { + filterAllSynapses: function(toVisible) { var self = Filter - $('#filter_by_mapper ul li').addClass('toggledOff') - $('.showAllMappers').removeClass('active') - $('.hideAllMappers').addClass('active') - self.visible.mappers = [] - self.passFilters() - }, - filterNoMappers: function(e) { - var self = Filter - $('#filter_by_mapper ul li').removeClass('toggledOff') - $('.showAllMappers').addClass('active') - $('.hideAllMappers').removeClass('active') - self.visible.mappers = self.filters.mappers.slice() - self.passFilters() - }, - filterAllSynapses: function(e) { - var self = Filter - $('#filter_by_synapse ul li').addClass('toggledOff') - $('.showAllSynapses').removeClass('active') - $('.hideAllSynapses').addClass('active') - self.visible.synapses = [] - self.passFilters() - }, - filterNoSynapses: function(e) { - var self = Filter - $('#filter_by_synapse ul li').removeClass('toggledOff') - $('.showAllSynapses').addClass('active') - $('.hideAllSynapses').removeClass('active') - self.visible.synapses = self.filters.synapses.slice() + self.visible.synapses = toVisible ? self.filters.synapses.slice() : [] + ReactApp.render() self.passFilters() }, // an abstraction function for toggleMetacode, toggleMapper, toggleSynapse // to reduce code redundancy // gets called in the context of a list item in a filter box - toggleLi: function(whichToFilter) { + toggleLi: function(whichToFilter, id) { var self = Filter - var id = $(this).attr('data-id') if (self.visible[whichToFilter].indexOf(id) === -1) { self.visible[whichToFilter].push(id) - $(this).removeClass('toggledOff') } else { const index = self.visible[whichToFilter].indexOf(id) self.visible[whichToFilter].splice(index, 1) - $(this).addClass('toggledOff') } + ReactApp.render() self.passFilters() }, - toggleMetacode: function() { + toggleMetacode: function(id) { var self = Filter - self.toggleLi.call(this, 'metacodes') - - if (self.visible.metacodes.length === self.filters.metacodes.length) { - $('.showAllMetacodes').addClass('active') - $('.hideAllMetacodes').removeClass('active') - } else if (self.visible.metacodes.length === 0) { - $('.showAllMetacodes').removeClass('active') - $('.hideAllMetacodes').addClass('active') - } else { - $('.showAllMetacodes').removeClass('active') - $('.hideAllMetacodes').removeClass('active') - } + self.toggleLi('metacodes', id) }, - toggleMapper: function() { + toggleMapper: function(id) { var self = Filter - self.toggleLi.call(this, 'mappers') - - if (self.visible.mappers.length === self.filters.mappers.length) { - $('.showAllMappers').addClass('active') - $('.hideAllMappers').removeClass('active') - } else if (self.visible.mappers.length === 0) { - $('.showAllMappers').removeClass('active') - $('.hideAllMappers').addClass('active') - } else { - $('.showAllMappers').removeClass('active') - $('.hideAllMappers').removeClass('active') - } + self.toggleLi('mappers', id) }, - toggleSynapse: function() { + toggleSynapse: function(id) { var self = Filter - self.toggleLi.call(this, 'synapses') - - if (self.visible.synapses.length === self.filters.synapses.length) { - $('.showAllSynapses').addClass('active') - $('.hideAllSynapses').removeClass('active') - } else if (self.visible.synapses.length === 0) { - $('.showAllSynapses').removeClass('active') - $('.hideAllSynapses').addClass('active') - } else { - $('.showAllSynapses').removeClass('active') - $('.hideAllSynapses').removeClass('active') - } + self.toggleLi('synapses', id) }, passFilters: function() { var self = Filter diff --git a/frontend/src/Metamaps/GlobalUI/Account.js b/frontend/src/Metamaps/GlobalUI/Account.js deleted file mode 100644 index a2823ef5..00000000 --- a/frontend/src/Metamaps/GlobalUI/Account.js +++ /dev/null @@ -1,55 +0,0 @@ -/* global $ */ - -import Filter from '../Filter' - -const Account = { - isOpen: false, - changing: false, - init: function() { - var self = Account - - $('.sidebarAccountIcon').click(self.toggleBox) - $('.sidebarAccountBox').click(function(event) { - event.stopPropagation() - }) - $('body').click(self.close) - }, - toggleBox: function(event) { - var self = Account - - if (self.isOpen) self.close() - else self.open() - - event.stopPropagation() - }, - open: function() { - var self = Account - - Filter.close() - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide') - - if (!self.isOpen && !self.changing) { - self.changing = true - $('.sidebarAccountBox').fadeIn(200, function() { - self.changing = false - self.isOpen = true - $('.sidebarAccountBox #user_email').focus() - }) - } - }, - close: function() { - var self = Account - - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') - if (!self.changing) { - self.changing = true - $('.sidebarAccountBox #user_email').blur() - $('.sidebarAccountBox').fadeOut(200, function() { - self.changing = false - self.isOpen = false - }) - } - } -} - -export default Account diff --git a/frontend/src/Metamaps/GlobalUI/CreateMap.js b/frontend/src/Metamaps/GlobalUI/CreateMap.js index a92e1836..c0c2868b 100644 --- a/frontend/src/Metamaps/GlobalUI/CreateMap.js +++ b/frontend/src/Metamaps/GlobalUI/CreateMap.js @@ -61,7 +61,7 @@ const CreateMap = { if (GlobalUI.lightbox === 'forkmap') { self.newMap.set('topicsToMap', self.topicsToMap) self.newMap.set('synapsesToMap', self.synapsesToMap) - self.newMap.set('source_id', Active.Map.id) + if (Active.Map) self.newMap.set('source_id', Active.Map.id) } var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' diff --git a/frontend/src/Metamaps/GlobalUI/ImportDialog.js b/frontend/src/Metamaps/GlobalUI/ImportDialog.js index 30215fa1..63c47491 100644 --- a/frontend/src/Metamaps/GlobalUI/ImportDialog.js +++ b/frontend/src/Metamaps/GlobalUI/ImportDialog.js @@ -4,7 +4,7 @@ import React from 'react' import ReactDOM from 'react-dom' import outdent from 'outdent' -import ImportDialogBox from '../../components/ImportDialogBox' +import ImportDialogBox from '../../components/MapView/ImportDialogBox' import PasteInput from '../PasteInput' import Map from '../Map' @@ -19,7 +19,7 @@ const ImportDialog = { self.closeLightbox = closeLightbox $('#lightbox_content').append($(outdent` -
    +
    `)) diff --git a/frontend/src/Metamaps/GlobalUI/NotificationIcon.js b/frontend/src/Metamaps/GlobalUI/NotificationIcon.js deleted file mode 100644 index 1e9ff3bd..00000000 --- a/frontend/src/Metamaps/GlobalUI/NotificationIcon.js +++ /dev/null @@ -1,30 +0,0 @@ -/* global $ */ - -import React from 'react' -import ReactDOM from 'react-dom' - -import Active from '../Active' -import NotificationIconComponent from '../../components/NotificationIcon' - -const NotificationIcon = { - unreadNotificationsCount: null, - - init: function(serverData) { - const self = NotificationIcon - self.unreadNotificationsCount = serverData.unreadNotificationsCount - self.render() - }, - render: function(newUnreadCount = null) { - if (newUnreadCount !== null) { - NotificationIcon.unreadNotificationsCount = newUnreadCount - } - - if (Active.Mapper !== null) { - ReactDOM.render(React.createElement(NotificationIconComponent, { - unreadNotificationsCount: NotificationIcon.unreadNotificationsCount - }), $('#notification_icon').get(0)) - } - } -} - -export default NotificationIcon diff --git a/frontend/src/Metamaps/GlobalUI/ReactApp.js b/frontend/src/Metamaps/GlobalUI/ReactApp.js new file mode 100644 index 00000000..eccdaa8a --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/ReactApp.js @@ -0,0 +1,234 @@ +/* global $ */ + +import React from 'react' +import ReactDOM from 'react-dom' +import { Router, browserHistory } from 'react-router' +import { merge } from 'lodash' + +import { notifyUser } from './index.js' +import ImportDialog from './ImportDialog' +import Active from '../Active' +import DataModel from '../DataModel' +import { ExploreMaps, ChatView, TopicCard } from '../Views' +import Filter from '../Filter' +import JIT from '../JIT' +import Realtime from '../Realtime' +import Map, { InfoBox } from '../Map' +import Topic from '../Topic' +import Visualize from '../Visualize' +import makeRoutes from '../../components/makeRoutes' +let routes + +// 220 wide + 16 padding on both sides +const MAP_WIDTH = 252 +const MOBILE_VIEW_BREAKPOINT = 504 +const MOBILE_VIEW_PADDING = 40 +const MAX_COLUMNS = 4 + +const ReactApp = { + mapId: null, + topicId: null, + unreadNotificationsCount: 0, + mapsWidth: 0, + toast: '', + mobile: false, + mobileTitle: '', + mobileTitleWidth: 0, + init: function(serverData, openLightbox) { + const self = ReactApp + self.unreadNotificationsCount = serverData.unreadNotificationsCount + self.mobileTitle = serverData.mobileTitle + self.openLightbox = openLightbox + routes = makeRoutes(serverData.ActiveMapper) + self.resize() + window && window.addEventListener('resize', self.resize) + }, + handleUpdate: function(location) { + const self = ReactApp + const pathname = this.state.location.pathname + switch (pathname.split('/')[1]) { + case '': + if (Active.Mapper && Active.Mapper.id) { + $('#yield').hide() + ExploreMaps.updateFromPath(pathname) + self.mapId = null + Active.Map = null + Active.Topic = null + } + break + case 'explore': + $('#yield').hide() + ExploreMaps.updateFromPath(pathname) + self.mapId = null + self.topicId = null + Active.Map = null + Active.Topic = null + break + case 'topics': + $('#yield').hide() + Active.Map = null + self.mapId = null + self.topicId = pathname.split('/')[2] + break + case 'maps': + if (!pathname.includes('request_access')) { + $('#yield').hide() + Active.Topic = null + self.topicId = null + self.mapId = pathname.split('/')[2] + } + break + default: + $('#yield').show() + break + } + self.render() + window.ga && window.ga('send', 'pageview', pathname) + }, + render: function() { + const self = ReactApp + const createElement = (Component, props) => + const app = + console.log('rendering') + ReactDOM.render(app, document.getElementById('react-app')) + }, + getProps: function() { + const self = ReactApp + return merge({ + unreadNotificationsCount: self.unreadNotificationsCount, + currentUser: Active.Mapper, + toast: self.toast, + mobile: self.mobile, + mobileTitle: self.mobileTitle, + mobileTitleWidth: self.mobileTitleWidth, + mobileTitleClick: (e) => Active.Map && InfoBox.toggleBox(e), + openInviteLightbox: () => self.openLightbox('invite') + }, + self.getMapProps(), + self.getTopicProps(), + self.getFilterProps(), + self.getCommonProps(), + self.getMapsProps(), + self.getTopicCardProps(), + self.getChatProps()) + }, + getMapProps: function() { + const self = ReactApp + return { + mapId: self.mapId, + map: Active.Map, + hasLearnedTopicCreation: Map.hasLearnedTopicCreation, + userRequested: Map.userRequested, + requestAnswered: Map.requestAnswered, + requestApproved: Map.requestApproved, + onRequestAccess: Map.requestAccess, + mapIsStarred: Map.mapIsStarred, + endActiveMap: Map.end, + launchNewMap: Map.launch, + toggleMapInfoBox: InfoBox.toggleBox, + infoBoxHtml: InfoBox.html, + openImportLightbox: () => ImportDialog.show(), + forkMap: Map.fork, + onMapStar: Map.star, + onMapUnstar: Map.unstar + } + }, + getCommonProps: function() { + const self = ReactApp + return { + openHelpLightbox: () => self.openLightbox('cheatsheet'), + onZoomExtents: event => JIT.zoomExtents(event, Visualize.mGraph.canvas), + onZoomIn: JIT.zoomIn, + onZoomOut: JIT.zoomOut + } + }, + getTopicCardProps: function() { + const self = ReactApp + return { + openTopic: TopicCard.openTopic, + metacodeSets: TopicCard.metacodeSets, + updateTopic: TopicCard.updateTopic, + onTopicFollow: TopicCard.onTopicFollow, + redrawCanvas: TopicCard.redrawCanvas + } + }, + getTopicProps: function() { + const self = ReactApp + return { + topicId: self.topicId, + topic: Active.Topic, + endActiveTopic: Topic.end, + launchNewTopic: Topic.launch + } + }, + getMapsProps: function() { + const self = ReactApp + return { + section: ExploreMaps.collection && ExploreMaps.collection.id, + maps: ExploreMaps.collection, + juntoState: Realtime.juntoState, + moreToLoad: ExploreMaps.collection && ExploreMaps.collection.page !== 'loadedAll', + user: ExploreMaps.collection && ExploreMaps.collection.id === 'mapper' ? ExploreMaps.mapper : null, + loadMore: ExploreMaps.loadMore, + pending: ExploreMaps.pending, + onStar: ExploreMaps.onStar, + onRequest: ExploreMaps.onRequest, + onMapFollow: ExploreMaps.onMapFollow, + mapsWidth: ReactApp.mapsWidth + } + }, + getChatProps: function() { + const self = ReactApp + return { + unreadMessages: ChatView.unreadMessages, + conversationLive: ChatView.conversationLive, + isParticipating: ChatView.isParticipating, + onOpen: ChatView.onOpen, + onClose: ChatView.onClose, + leaveCall: Realtime.leaveCall, + joinCall: Realtime.joinCall, + inviteACall: Realtime.inviteACall, + inviteToJoin: Realtime.inviteToJoin, + participants: ChatView.participants ? ChatView.participants.models.map(p => p.attributes) : [], + messages: ChatView.messages ? ChatView.messages.models.map(m => m.attributes) : [], + videoToggleClick: ChatView.videoToggleClick, + cursorToggleClick: ChatView.cursorToggleClick, + soundToggleClick: ChatView.soundToggleClick, + inputBlur: ChatView.inputBlur, + inputFocus: ChatView.inputFocus, + handleInputMessage: ChatView.handleInputMessage + } + }, + getFilterProps: function() { + const self = ReactApp + return { + filterData: Filter.dataForPresentation, + allForFiltering: Filter.filters, + visibleForFiltering: Filter.visible, + toggleMetacode: Filter.toggleMetacode, + toggleMapper: Filter.toggleMapper, + toggleSynapse: Filter.toggleSynapse, + filterAllMetacodes: Filter.filterAllMetacodes, + filterAllMappers: Filter.filterAllMappers, + filterAllSynapses: Filter.filterAllSynapses + } + }, + resize: function() { + const self = ReactApp + const maps = ExploreMaps.collection + const currentUser = Active.Mapper + const user = maps && maps.id === 'mapper' ? ExploreMaps.mapper : null + const numCards = (maps ? maps.length : 0) + (user || currentUser ? 1 : 0) + const mapSpaces = Math.floor(document.body.clientWidth / MAP_WIDTH) + const mapsWidth = document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT + ? document.body.clientWidth - MOBILE_VIEW_PADDING + : Math.min(MAX_COLUMNS, Math.min(numCards, mapSpaces)) * MAP_WIDTH + + self.mapsWidth = mapsWidth + self.mobileTitleWidth = document ? document.body.clientWidth - 70 : 0 + self.mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT + self.render() + } +} + +export default ReactApp diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js index a37b62a8..7e28906c 100644 --- a/frontend/src/Metamaps/GlobalUI/Search.js +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -1,7 +1,8 @@ /* global $, Hogan, Bloodhound, CanvasLoader */ +import { browserHistory } from 'react-router' + import Active from '../Active' -import Router from '../Router' const Search = { locked: false, @@ -17,6 +18,7 @@ const Search = { self.userIconUrl = serverData['user.png'] // this is similar to Metamaps.Loading, but it's for the search element + if (!document.getElementById('searchLoading')) return var loader = new CanvasLoader('searchLoading') loader.setColor('#4fb5c0') // default is '#000000' loader.setDiameter(24) // default is 40 @@ -189,11 +191,11 @@ const Search = { if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { if (datum.rtype === 'topic') { - Router.topics(datum.id) + browserHistory.push(`/topics/${datum.id}`) } else if (datum.rtype === 'map') { - Router.maps(datum.id) + browserHistory.push(`/maps/${datum.id}`) } else if (datum.rtype === 'mapper') { - Router.explore('mapper', datum.id) + browserHistory.push(`/explore/mapper/${datum.id}`) } } }, diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index a1d2bbff..b2dd6654 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -4,11 +4,10 @@ import clipboard from 'clipboard-js' import Create from '../Create' +import ReactApp from './ReactApp' import Search from './Search' import CreateMap from './CreateMap' -import Account from './Account' import ImportDialog from './ImportDialog' -import NotificationIcon from './NotificationIcon' const GlobalUI = { notifyTimeout: null, @@ -18,13 +17,12 @@ const GlobalUI = { init: function(serverData) { const self = GlobalUI - self.Search.init(serverData) + self.ReactApp.init(serverData, self.openLightbox) self.CreateMap.init(serverData) - self.Account.init(serverData) self.ImportDialog.init(serverData, self.openLightbox, self.closeLightbox) - self.NotificationIcon.init(serverData) + self.Search.init(serverData) - if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) + if (serverData.toast) self.notifyUser(serverData.toast) // bind lightbox clicks $('.openLightbox').click(function(event) { @@ -112,10 +110,9 @@ const GlobalUI = { _notifyUser: function(message, opts = {}) { const self = GlobalUI - const { leaveOpen = false, timeOut = 8000 } = opts - - $('#toast').html(message) - self.showDiv('#toast') + const { leaveOpen = false, timeOut = 5000 } = opts + ReactApp.toast = message + ReactApp.render() clearTimeout(self.notifyTimeOut) if (!leaveOpen) { @@ -134,7 +131,8 @@ const GlobalUI = { const { message, opts } = self.notifyQueue.shift() self._notifyUser(message, opts) } else { - self.hideDiv('#toast') + ReactApp.toast = null + ReactApp.render() self.notifying = false } }, @@ -153,5 +151,5 @@ const GlobalUI = { } } -export { Search, CreateMap, Account, ImportDialog, NotificationIcon } +export { ReactApp, Search, CreateMap, ImportDialog } export default GlobalUI diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 9830eace..9b90b993 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -316,7 +316,7 @@ const Import = { success: opts.success }) - GlobalUI.hideDiv('#instructions') + Map.setHasLearnedTopicCreation(true) }, createSynapseWithParameters: function(desc, category, permission, diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index eb023fe8..8100ebfe 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -50,15 +50,6 @@ const JIT = { */ init: function(serverData) { const self = JIT - - $('.zoomIn').click(self.zoomIn) - $('.zoomOut').click(self.zoomOut) - - const zoomExtents = function(event) { - self.zoomExtents(event, Visualize.mGraph.canvas) - } - $('.zoomExtents').click(zoomExtents) - self.topicDescImage = new Image() self.topicDescImage.src = serverData['topic_description_signifier.png'] @@ -123,36 +114,22 @@ const JIT = { prepareVizData: function() { const self = JIT let mapping - - // reset/empty vizData self.vizData = [] Visualize.loadLater = false - const results = self.convertModelsToJIT(DataModel.Topics, DataModel.Synapses) - self.vizData = results[0] - // clean up the synapses array in case of any faulty data _.each(results[1], function(synapse) { mapping = synapse.getMapping() DataModel.Synapses.remove(synapse) if (DataModel.Mappings) DataModel.Mappings.remove(mapping) }) - - // set up addTopic instructions in case they delete all the topics - // i.e. if there are 0 topics at any time, it should have instructions again - $('#instructions div').hide() - if (Active.Map && Active.Map.authorizeToEdit(Active.Mapper)) { - $('#instructions div.addTopic').show() - } - if (self.vizData.length === 0) { - GlobalUI.showDiv('#instructions') + Map.setHasLearnedTopicCreation(false) Visualize.loadLater = true } else { - GlobalUI.hideDiv('#instructions') + Map.setHasLearnedTopicCreation(true) } - Visualize.render() }, // prepareVizData edgeRender: function(adj, canvas) { @@ -1026,7 +1003,6 @@ const JIT = { Create.newTopic.open() } else if (!Mouse.didPan) { // SINGLE CLICK, no pan - Filter.close() TopicCard.hideCard() SynapseCard.hideCard() Create.newTopic.hide() diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 4a2a1f56..b31218e1 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -146,7 +146,6 @@ const Listeners = { } if (Active.Map && Realtime.inConversation) Realtime.positionVideos() - Mobile.resizeTitle() }) }, centerAndReveal: function(nodes, opts) { diff --git a/frontend/src/Metamaps/Loading.js b/frontend/src/Metamaps/Loading.js index 50d2191b..d5365b4c 100644 --- a/frontend/src/Metamaps/Loading.js +++ b/frontend/src/Metamaps/Loading.js @@ -15,6 +15,7 @@ const Loading = { Loading.loader.setDensity(41) // default is 40 Loading.loader.setRange(0.9) // default is 1.3 Loading.loader.show() // Hidden by default + $('#loading').hide() } } diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index 1949e96e..3f6fcd9c 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,16 +1,15 @@ /* global $, Hogan, Bloodhound, Countable */ import outdent from 'outdent' +import { browserHistory } from 'react-router' import Active from '../Active' import DataModel from '../DataModel' -import GlobalUI from '../GlobalUI' -import Router from '../Router' +import GlobalUI, { ReactApp } from '../GlobalUI' import Util from '../Util' const InfoBox = { isOpen: false, - changing: false, selectingPermission: false, changePermissionText: "
    As the creator, you can change the permission of this map, and the permission of all the topics and synapses you have authority to change will change as well.
    ", nameHTML: outdent` @@ -35,12 +34,12 @@ const InfoBox = { data-bip-value="{{desc}}" >{{desc}}`, userImageUrl: '', + html: '', init: function(serverData, updateThumbnail) { var self = InfoBox self.updateThumbnail = updateThumbnail - $('.mapInfoIcon').click(self.toggleBox) $('.mapInfoBox').click(function(event) { event.stopPropagation() }) @@ -72,27 +71,18 @@ const InfoBox = { open: function() { var self = InfoBox $('.mapInfoIcon div').addClass('hide') - if (!self.isOpen && !self.changing) { - self.changing = true - $('.mapInfoBox').fadeIn(200, function() { - self.changing = false - self.isOpen = true - }) - } + $('.mapInfoBox').fadeIn(200, function() { + self.isOpen = true + }) }, close: function() { var self = InfoBox - $('.mapInfoIcon div').removeClass('hide') - if (!self.changing) { - self.changing = true - $('.mapInfoBox').fadeOut(200, function() { - self.changing = false - self.isOpen = false - self.hidePermissionSelect() - $('.mapContributors .tip').hide() - }) - } + $('.mapInfoBox').fadeOut(200, function() { + self.isOpen = false + self.hidePermissionSelect() + $('.mapContributors .tip').hide() + }) }, load: function() { var self = InfoBox @@ -120,13 +110,8 @@ const InfoBox = { obj['created_at'] = map.get('created_at_clean') obj['updated_at'] = map.get('updated_at_clean') - var classes = isCreator ? 'yourMap' : '' - classes += canEdit ? ' canEdit' : '' - classes += shareable ? ' shareable' : '' - $('.mapInfoBox').removeClass('shareable yourMap canEdit') - .addClass(classes) - .html(self.generateBoxHTML.render(obj)) - + self.html = self.generateBoxHTML.render(obj) + ReactApp.render() self.attachEventListeners() }, attachEventListeners: function() { @@ -192,7 +177,6 @@ const InfoBox = { $('.mapContributors .tip').unbind().click(function(event) { event.stopPropagation() }) - $('.mapContributors .tip li a').click(Router.intercept) $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function() { $('.mapContributors .tip').hide() @@ -393,7 +377,7 @@ const InfoBox = { DataModel.Maps.Mine.remove(map) DataModel.Maps.Shared.remove(map) map.destroy() - Router.home() + browserHistory.push('/') GlobalUI.notifyUser('Map eliminated') } else if (!authorized) { window.alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index e3c3bbc6..2db12263 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -2,6 +2,7 @@ import outdent from 'outdent' import { find as _find } from 'lodash' +import { browserHistory } from 'react-router' import Active from '../Active' import AutoLayout from '../AutoLayout' @@ -9,11 +10,10 @@ import Create from '../Create' import DataModel from '../DataModel' import DataModelMap from '../DataModel/Map' import Filter from '../Filter' -import GlobalUI from '../GlobalUI' +import GlobalUI, { ReactApp } from '../GlobalUI' import JIT from '../JIT' import Loading from '../Loading' import Realtime from '../Realtime' -import Router from '../Router' import Selected from '../Selected' import SynapseCard from '../SynapseCard' import TopicCard from '../Views/TopicCard' @@ -26,143 +26,126 @@ const Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' }, + mapIsStarred: false, + requests: [], + userRequested: false, + requestAnswered: false, + requestApproved: false, + hasLearnedTopicCreation: true, init: function(serverData) { var self = Map - + self.mapIsStarred = serverData.mapIsStarred + self.requests = serverData.requests + self.setAccessRequest() $('#wrapper').mousedown(function(e) { if (e.button === 1) return false }) - - $('.starMap').click(function() { - if ($(this).is('.starred')) self.unstar() - else self.star() - }) - - $('.sidebarFork').click(function() { - self.fork() - }) - GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() - - self.updateStar() - InfoBox.init(serverData, function updateThumbnail() { self.uploadMapScreenshot() }) CheatSheet.init(serverData) - - $('.viewOnly .requestAccess').click(self.requestAccess) - $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) }, + setHasLearnedTopicCreation: function(value) { + const self = Map + self.hasLearnedTopicCreation = value + ReactApp.render() + }, requestAccess: function() { - $('.viewOnly').removeClass('sendRequest').addClass('sentRequest') + const self = Map + self.requests.push({ + user_id: Active.Mapper.id, + answered: false, + approved: false + }) + self.setAccessRequest() const mapId = Active.Map.id $.post({ url: `/maps/${mapId}/access_request` }) GlobalUI.notifyUser('Map creator will be notified of your request') }, - setAccessRequest: function(requests, activeMapper) { - let className = 'isViewOnly ' - if (activeMapper) { - const request = _find(requests, r => r.user_id === activeMapper.id) - if (!request) className += 'sendRequest' - else if (request && !request.answered) className += 'sentRequest' - else if (request && request.answered && !request.approved) className += 'requestDenied' + setAccessRequest: function() { + const self = Map + if (Active.Mapper) { + const request = _find(self.requests, r => r.user_id === Active.Mapper.id) + if (!request) { + self.userRequested = false + self.requestAnswered = false + self.requestApproved = false + } + else if (request && !request.answered) { + self.userRequested = true + self.requestAnswered = false + self.requestApproved = false + } + else if (request && request.answered && !request.approved) { + self.userRequested = true + self.requestAnswered = true + self.requestApproved = false + } } - $('.viewOnly').removeClass('sendRequest sentRequest requestDenied').addClass(className) + ReactApp.render() }, launch: function(id) { - var start = function(data) { - Active.Map = new DataModelMap(data.map) - DataModel.Mappers = new DataModel.MapperCollection(data.mappers) - DataModel.Collaborators = new DataModel.MapperCollection(data.collaborators) - DataModel.Topics = new DataModel.TopicCollection(data.topics) - DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) - DataModel.Mappings = new DataModel.MappingCollection(data.mappings) - DataModel.Messages = data.messages - DataModel.Stars = data.stars - DataModel.attachCollectionEvents() - - var map = Active.Map - var mapper = Active.Mapper - - document.title = map.get('name') + ' | Metamaps' - - // add class to .wrapper for specifying whether you can edit the map - if (map.authorizeToEdit(mapper)) { - $('.wrapper').addClass('canEditMap') - } else { - Map.setAccessRequest(data.requests, mapper) - } - - // add class to .wrapper for specifying if the map can - // be collaborated on - if (map.get('permission') === 'commons') { - $('.wrapper').addClass('commonsMap') - } - - Map.updateStar() - - // set filter mapper H3 text - $('#filter_by_mapper h3').html('MAPPERS') - - // build and render the visualization + const self = Map + var dataIsReadySetupMap = function() { + Map.setAccessRequest() Visualize.type = 'ForceDirected' JIT.prepareVizData() - - // update filters - Filter.reset() - - // reset selected arrays Selected.reset() - - // set the proper mapinfobox content InfoBox.load() - - // these three update the actual filter box with the right list items + Filter.reset() Filter.checkMetacodes() Filter.checkSynapses() Filter.checkMappers() - Realtime.startActiveMap() Loading.hide() - - // for mobile - $('#header_content').html(map.get('name')) + document.title = Active.Map.get('name') + ' | Metamaps' + ReactApp.mobileTitle = Active.Map.get('name') + ReactApp.render() + } + function isLoaded() { + if (InfoBox.generateBoxHTML) dataIsReadySetupMap() + else setTimeout(() => isLoaded(), 50) + } + if (Active.Map && Active.Map.id === id) { + isLoaded() + } + else { + Loading.show() + $.ajax({ + url: '/maps/' + id + '/contains.json', + success: function(data) { + Active.Map = new DataModelMap(data.map) + DataModel.Mappers = new DataModel.MapperCollection(data.mappers) + DataModel.Collaborators = new DataModel.MapperCollection(data.collaborators) + DataModel.Topics = new DataModel.TopicCollection(data.topics) + DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) + DataModel.Mappings = new DataModel.MappingCollection(data.mappings) + DataModel.Messages = data.messages + DataModel.Stars = data.stars + DataModel.attachCollectionEvents() + self.requests = data.requests + isLoaded() + } + }) } - - $.ajax({ - url: '/maps/' + id + '/contains.json', - success: start - }) }, end: function() { if (Active.Map) { - $('.wrapper').removeClass('canEditMap commonsMap') + $('.main').removeClass('compressed') AutoLayout.resetSpiral() - $('.rightclickmenu').remove() TopicCard.hideCard() SynapseCard.hideCard() Create.newTopic.hide(true) // true means force (and override pinned) Create.newSynapse.hide() - Filter.close() InfoBox.close() Realtime.endActiveMap() - $('.viewOnly').removeClass('isViewOnly') - } - }, - updateStar: function() { - if (!Active.Mapper || !DataModel.Stars) return - // update the star/unstar icon - if (DataModel.Stars.find(function(s) { return s.user_id === Active.Mapper.id })) { - $('.starMap').addClass('starred') - $('.starMap .tooltipsAbove').html('Unstar') - } else { - $('.starMap').removeClass('starred') - $('.starMap .tooltipsAbove').html('Star') + self.requests = [] + self.hasLearnedTopicCreation = true } }, star: function() { @@ -173,7 +156,8 @@ const Map = { DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) DataModel.Maps.Starred.add(Active.Map) GlobalUI.notifyUser('Map is now starred') - self.updateStar() + self.mapIsStarred = true + ReactApp.render() }, unstar: function() { var self = Map @@ -182,7 +166,8 @@ const Map = { $.post('/maps/' + Active.Map.id + '/unstar') DataModel.Stars = DataModel.Stars.filter(function(s) { return s.user_id !== Active.Mapper.id }) DataModel.Maps.Starred.remove(Active.Map) - self.updateStar() + self.mapIsStarred = false + ReactApp.render() }, fork: function() { GlobalUI.openLightbox('forkmap') @@ -232,7 +217,7 @@ const Map = { var map = Active.Map DataModel.Maps.Active.remove(map) DataModel.Maps.Featured.remove(map) - Router.home() + browserHistory.push('/') GlobalUI.notifyUser('Sorry! That map has been changed to Private.') }, cantEditNow: function() { @@ -245,7 +230,7 @@ const Map = { confirmString += 'Do you want to reload and enable realtime collaboration?' var c = window.confirm(confirmString) if (c) { - Router.maps(Active.Map.id) + window.location.reload() } }, editedByActiveMapper: function() { diff --git a/frontend/src/Metamaps/Realtime/index.js b/frontend/src/Metamaps/Realtime/index.js index b73cf52c..9e15ef62 100644 --- a/frontend/src/Metamaps/Realtime/index.js +++ b/frontend/src/Metamaps/Realtime/index.js @@ -151,8 +151,6 @@ let Realtime = { config: { DOUBLE_CLICK_TOLERANCE: 200 } }) self.room.videoAdded(self.handleVideoAdded) - - self.startActiveMap() } // if Active.Mapper }, addJuntoListeners: function() { @@ -201,9 +199,6 @@ let Realtime = { self.leaveMap() $('.collabCompass').remove() if (self.room) self.room.leave() - ChatView.hide() - ChatView.close() - ChatView.reset() Cable.unsubscribeFromMap() }, turnOn: function(notify) { @@ -228,7 +223,6 @@ let Realtime = { ChatView.setNewMap() ChatView.addParticipant(self.activeMapper) ChatView.addMessages(new DataModel.MessageCollection(DataModel.Messages), true) - ChatView.show() }, setupLocalEvents: function() { var self = Realtime diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js deleted file mode 100644 index 6df1c264..00000000 --- a/frontend/src/Metamaps/Router.js +++ /dev/null @@ -1,244 +0,0 @@ -/* global $ */ - -import Backbone from 'backbone' -try { Backbone.$ = window.$ } catch (err) {} - -import Active from './Active' -import DataModel from './DataModel' -import GlobalUI from './GlobalUI' -import Loading from './Loading' -import Map from './Map' -import Topic from './Topic' -import Views from './Views' -import Visualize from './Visualize' - -const _Router = Backbone.Router.extend({ - currentPage: '', - currentSection: '', - timeoutId: undefined, - routes: { - '': 'home', // #home - 'explore/:section': 'explore', // #explore/active - 'explore/:section/:id': 'explore', // #explore/mapper/1234 - 'maps/:id': 'maps', // #maps/7 - 'topics/:id': 'topics' // #topics/7 - }, - home: function() { - let self = this - clearTimeout(this.timeoutId) - - if (Active.Mapper) document.title = 'Explore Active Maps | Metamaps' - else document.title = 'Home | Metamaps' - - this.currentSection = '' - this.currentPage = '' - $('.wrapper').removeClass('mapPage topicPage') - - var classes = Active.Mapper ? 'homePage explorePage' : 'homePage' - $('.wrapper').addClass(classes) - - var navigate = function() { - self.timeoutId = setTimeout(function() { - self.navigateAndTrack('') - }, 300) - } - - // all this only for the logged in home page - if (Active.Mapper) { - $('.homeButton a').attr('href', '/') - GlobalUI.hideDiv('#yield') - - GlobalUI.showDiv('#explore') - - Views.ExploreMaps.setCollection(DataModel.Maps.Active) - if (DataModel.Maps.Active.length === 0) { - Views.ExploreMaps.pending = true - DataModel.Maps.Active.getMaps(navigate) // this will trigger an explore maps render - } else { - Views.ExploreMaps.render(navigate) - } - } else { - // logged out home page - GlobalUI.hideDiv('#explore') - GlobalUI.showDiv('#yield') - this.timeoutId = setTimeout(navigate, 500) - } - - GlobalUI.hideDiv('#infovis') - GlobalUI.hideDiv('#instructions') - Map.end() - Topic.end() - Active.Map = null - Active.Topic = null - }, - explore: function(section, id) { - var self = this - clearTimeout(this.timeoutId) - - // just capitalize the variable section - // either 'featured', 'mapper', or 'active' - var capitalize = section.charAt(0).toUpperCase() + section.slice(1) - - if (section === 'shared' || section === 'featured' || section === 'active' || section === 'starred') { - document.title = 'Explore ' + capitalize + ' Maps | Metamaps' - } else if (section === 'mapper') { - $.ajax({ - url: '/users/' + id + '.json', - success: function(response) { - document.title = response.name + ' | Metamaps' - }, - error: function() {} - }) - } else if (section === 'mine') { - document.title = 'Explore My Maps | Metamaps' - } - - if (Active.Mapper && section !== 'mapper') $('.homeButton a').attr('href', '/explore/' + section) - $('.wrapper').removeClass('homePage mapPage topicPage') - $('.wrapper').addClass('explorePage') - - this.currentSection = 'explore' - this.currentPage = section - - // this will mean it's a mapper page being loaded - if (id) { - if (DataModel.Maps.Mapper.mapperId !== id) { - // empty the collection if we are trying to load the maps - // collection of a different mapper than we had previously - DataModel.Maps.Mapper.reset() - DataModel.Maps.Mapper.page = 1 - } - DataModel.Maps.Mapper.mapperId = id - } - - Views.ExploreMaps.setCollection(DataModel.Maps[capitalize]) - - var navigate = function() { - var path = '/explore/' + self.currentPage - - // alter url if for mapper profile page - if (self.currentPage === 'mapper') { - path += '/' + DataModel.Maps.Mapper.mapperId - } - - self.navigateAndTrack(path) - } - var navigateTimeout = function() { - self.timeoutId = setTimeout(navigate, 300) - } - if (DataModel.Maps[capitalize].length === 0) { - Loading.show() - Views.ExploreMaps.pending = true - setTimeout(function() { - DataModel.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render - }, 300) // wait 300 milliseconds till the other animations are done to do the fetch - } else { - if (id) { - Views.ExploreMaps.fetchUserThenRender(navigateTimeout) - } else { - Views.ExploreMaps.render(navigateTimeout) - } - } - - GlobalUI.showDiv('#explore') - GlobalUI.hideDiv('#yield') - GlobalUI.hideDiv('#infovis') - GlobalUI.hideDiv('#instructions') - Map.end() - Topic.end() - Active.Map = null - Active.Topic = null - }, - maps: function(id) { - clearTimeout(this.timeoutId) - - this.currentSection = 'map' - this.currentPage = id - - $('.wrapper').removeClass('homePage explorePage topicPage') - $('.wrapper').addClass('mapPage') - // another class will be added to wrapper if you - // can edit this map '.canEditMap' - - GlobalUI.hideDiv('#yield') - GlobalUI.hideDiv('#explore') - - // clear the visualization, if there was one, before showing its div again - if (Visualize.mGraph) { - Visualize.clearVisualization() - } - GlobalUI.showDiv('#infovis') - Topic.end() - Active.Topic = null - - Loading.show() - Map.end() - Map.launch(id) - }, - topics: function(id) { - clearTimeout(this.timeoutId) - - this.currentSection = 'topic' - this.currentPage = id - - $('.wrapper').removeClass('homePage explorePage mapPage') - $('.wrapper').addClass('topicPage') - - GlobalUI.hideDiv('#yield') - GlobalUI.hideDiv('#explore') - - // clear the visualization, if there was one, before showing its div again - if (Visualize.mGraph) { - Visualize.clearVisualization() - } - GlobalUI.showDiv('#infovis') - Map.end() - Active.Map = null - - Topic.end() - Topic.launch(id) - } -}) - -const Router = new _Router() - -Router.navigateAndTrack = (fragment, options) => { - Router.navigate(fragment, options) - window.ga && window.ga('send', 'pageview', location.pathname, {title: document.title}) -} - -Router.intercept = function(evt) { - var segments - - var href = { - prop: $(this).prop('href'), - attr: $(this).attr('href') - } - var root = window.location.protocol + '//' + window.location.host + Backbone.history.options.root - - if (href.prop && href.prop === root) href.attr = '' - - if (href.prop && href.prop.slice(0, root.length) === root) { - evt.preventDefault() - - segments = href.attr.split('/') - segments.splice(0, 1) // pop off the element created by the first / - - if (href.attr === '') { - Router.home() - } else { - Router[segments[0]](segments[1], segments[2]) - } - } -} - -Router.init = function() { - Backbone.history.start({ - silent: true, - pushState: true, - root: '/' - }) - $(document).on('click', 'a[data-router="true"]', Router.intercept) -} - -export default Router diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 663b0c11..49b27253 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -7,10 +7,10 @@ import AutoLayout from './AutoLayout' import Create from './Create' import DataModel from './DataModel' import Filter from './Filter' -import GlobalUI from './GlobalUI' +import GlobalUI, { ReactApp } from './GlobalUI' import JIT from './JIT' +import Loading from './Loading' import Map from './Map' -import Router from './Router' import Selected from './Selected' import Settings from './Settings' import SynapseCard from './SynapseCard' @@ -36,48 +36,41 @@ const Topic = { } else callback(DataModel.Topics.get(id)) }, launch: function(id) { - var start = function(data) { - Active.Topic = new DataModel.Topic(data.topic) - DataModel.Creators = new DataModel.MapperCollection(data.creators) - DataModel.Topics = new DataModel.TopicCollection([data.topic].concat(data.relatives)) - DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) - DataModel.attachCollectionEvents() - - document.title = Active.Topic.get('name') + ' | Metamaps' - - // set filter mapper H3 text - $('#filter_by_mapper h3').html('CREATORS') - - // build and render the visualization + var dataIsReadySetupTopic = function() { Visualize.type = 'RGraph' JIT.prepareVizData() - - // update filters - Filter.reset() - - // reset selected arrays Selected.reset() - - // these three update the actual filter box with the right list items + Filter.reset() Filter.checkMetacodes() Filter.checkSynapses() Filter.checkMappers() - - // for mobile - $('#header_content').html(Active.Topic.get('name')) + document.title = Active.Topic.get('name') + ' | Metamaps' + ReactApp.mobileTitle = Active.Topic.get('name') + ReactApp.render() + } + if (Active.Topic && Active.Topic.id === id) { + dataIsReadySetupTopic() + } + else { + Loading.show() + $.ajax({ + url: '/topics/' + id + '/network.json', + success: function(data) { + Active.Topic = new DataModel.Topic(data.topic) + DataModel.Creators = new DataModel.MapperCollection(data.creators) + DataModel.Topics = new DataModel.TopicCollection([data.topic].concat(data.relatives)) + DataModel.Synapses = new DataModel.SynapseCollection(data.synapses) + DataModel.attachCollectionEvents() + dataIsReadySetupTopic() + } + }) } - - $.ajax({ - url: '/topics/' + id + '/network.json', - success: start - }) }, end: function() { if (Active.Topic) { $('.rightclickmenu').remove() TopicCard.hideCard() SynapseCard.hideCard() - Filter.close() } }, centerOn: function(nodeid, callback) { @@ -90,7 +83,6 @@ const Topic = { if (callback) callback() } }) - Router.navigate('/topics/' + nodeid) Active.Topic = DataModel.Topics.get(nodeid) } }, @@ -293,8 +285,7 @@ const Topic = { return } - // hide the 'double-click to add a topic' message - GlobalUI.hideDiv('#instructions') + Map.setHasLearnedTopicCreation(true) $(document).trigger(Map.events.editedByActiveMapper) @@ -327,8 +318,7 @@ const Topic = { getTopicFromAutocomplete: function(id) { var self = Topic - // hide the 'double-click to add a topic' message - GlobalUI.hideDiv('#instructions') + Map.setHasLearnedTopicCreation(true) $(document).trigger(Map.events.editedByActiveMapper) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 0024a623..186a61f6 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -10,14 +10,14 @@ import ReactDOM from 'react-dom' import Active from '../Active' import DataModel from '../DataModel' import Realtime from '../Realtime' -import MapChat from '../../components/MapChat' +import ReactApp from '../GlobalUI/ReactApp' const ChatView = { isOpen: false, + unreadMessages: 0, messages: new Backbone.Collection(), conversationLive: false, isParticipating: false, - mapChat: null, domId: 'chat-box-wrapper', init: function(urls) { const self = ChatView @@ -34,46 +34,32 @@ const ChatView = { }, setNewMap: function() { const self = ChatView + self.unreadMessages = 0 + self.isOpen = false self.conversationLive = false self.isParticipating = false self.alertSound = true // whether to play sounds on arrival of new messages or not self.cursorsShowing = true self.videosShowing = true self.participants = new Backbone.Collection() + self.messages = new Backbone.Collection() self.render() }, - show: () => { - $('#' + ChatView.domId).show() - }, - hide: () => { - $('#' + ChatView.domId).hide() - }, render: () => { if (!Active.Map) return const self = ChatView - self.mapChat = ReactDOM.render(React.createElement(MapChat, { - conversationLive: self.conversationLive, - isParticipating: self.isParticipating, - onOpen: self.onOpen, - onClose: self.onClose, - leaveCall: Realtime.leaveCall, - joinCall: Realtime.joinCall, - inviteACall: Realtime.inviteACall, - inviteToJoin: Realtime.inviteToJoin, - participants: self.participants.models.map(p => p.attributes), - messages: self.messages.models.map(m => m.attributes), - videoToggleClick: self.videoToggleClick, - cursorToggleClick: self.cursorToggleClick, - soundToggleClick: self.soundToggleClick, - inputBlur: self.inputBlur, - inputFocus: self.inputFocus, - handleInputMessage: self.handleInputMessage - }), document.getElementById(ChatView.domId)) + ReactApp.render() }, onOpen: () => { + const self = ChatView + self.isOpen = true + self.unreadMessages = 0 + self.render() $(document).trigger(ChatView.events.openTray) }, onClose: () => { + const self = ChatView + self.isOpen = false $(document).trigger(ChatView.events.closeTray) }, addParticipant: participant => { @@ -119,12 +105,6 @@ const ChatView = { ChatView.participants.forEach(p => p.set({isParticipating: false, isPending: false})) ChatView.render() }, - close: () => { - ChatView.mapChat && ChatView.mapChat.close() - }, - open: () => { - ChatView.mapChat && ChatView.mapChat.open() - }, videoToggleClick: function() { ChatView.videosShowing = !ChatView.videosShowing $(document).trigger(ChatView.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff) @@ -144,11 +124,10 @@ const ChatView = { }, addMessage: (message, isInitial, wasMe) => { const self = ChatView - if (!isInitial) self.mapChat.newMessage() + if (!isInitial && !self.isOpen) self.unreadMessages += 1 if (!wasMe && !isInitial && self.alertSound) self.sound.play('receivechat') self.messages.add(message) - self.render() - if (!isInitial) self.mapChat.scroll() + if (!isInitial && self.isOpen) self.render() }, sendChatMessage: message => { var self = ChatView @@ -174,23 +153,9 @@ const ChatView = { // passed to this function addMessages: (messages, isInitial, wasMe) => { messages.models.forEach(m => ChatView.addMessage(m, isInitial, wasMe)) - }, - reset: () => { - ChatView.mapChat && ChatView.mapChat.reset() - ChatView.participants && ChatView.participants.reset() - ChatView.messages && ChatView.messages.reset() - ChatView.render() } } -// ChatView.prototype.scrollMessages = function(duration) { -// duration = duration || 0 - -// this.$messages.animate({ -// scrollTop: this.$messages[0].scrollHeight -// }, duration) -// } - /** * @class * @static diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index 5c10691b..ac546f71 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -1,21 +1,68 @@ /* global $ */ -import React from 'react' -import ReactDOM from 'react-dom' // TODO ensure this isn't a double import - import Active from '../Active' import DataModel from '../DataModel' -import GlobalUI from '../GlobalUI' -import Realtime from '../Realtime' +import GlobalUI, { ReactApp } from '../GlobalUI' import Loading from '../Loading' -import Maps from '../../components/Maps' const ExploreMaps = { pending: false, mapper: null, + updateFromPath: function(path) { + const self = ExploreMaps + const [_unused, generalSection, specificSection, id] = path.split('/') + + if (generalSection === 'explore') { + const capitalize = specificSection.charAt(0).toUpperCase() + specificSection.slice(1) + self.setCollection(DataModel.Maps[capitalize]) + switch (capitalize) { + case 'Active': + document.title = 'Explore Active Maps | Metamaps' + ReactApp.mobileTitle = 'Recently Active' + break + case 'Featured': + document.title = 'Explore Featured Maps | Metamaps' + ReactApp.mobileTitle = 'Featured Maps' + break + case 'Starred': + document.title = 'Starred Maps | Metamaps' + ReactApp.mobileTitle = 'Starred Maps' + break + case 'Shared': + document.title = 'Shared Maps | Metamaps' + ReactApp.mobileTitle = 'Shared With Me' + break + case 'Mine': + document.title = 'My Maps | Metamaps' + ReactApp.mobileTitle = 'My Maps' + break + } + } else if (generalSection === '') { + self.setCollection(DataModel.Maps.Active) + document.title = 'Explore Active Maps | Metamaps' + ReactApp.mobileTitle = 'Recently Active' + } + + if (id) { + if (self.collection.mapperId !== id) { + // empty the collection if we are trying to load the maps + // collection of a different mapper than we had previously + self.collection.reset() + self.collection.page = 1 + self.render() + } + self.collection.mapperId = id + } + if (self.collection.length === 0) { + Loading.show() + self.pending = true + self.collection.getMaps() + } else { + id ? self.fetchUserThenRender() : self.render() + } + }, setCollection: function(collection) { var self = ExploreMaps - if (self.collection) { self.collection.off('add', self.render) self.collection.off('successOnFetch', self.handleSuccess) @@ -26,55 +73,9 @@ const ExploreMaps = { self.collection.on('successOnFetch', self.handleSuccess) self.collection.on('errorOnFetch', self.handleError) }, - render: function(cb) { - var self = ExploreMaps - - if (!self.collection) return - - var exploreObj = { - currentUser: Active.Mapper, - section: self.collection.id, - maps: self.collection, - juntoState: Realtime.juntoState, - moreToLoad: self.collection.page !== 'loadedAll', - user: self.collection.id === 'mapper' ? self.mapper : null, - loadMore: self.loadMore, - pending: self.pending, - onStar: function(map) { - $.post('/maps/' + map.id + '/star') - map.set('star_count', map.get('star_count') + 1) - if (DataModel.Stars) DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: map.id }) - DataModel.Maps.Starred.add(map) - GlobalUI.notifyUser('Map is now starred') - self.render() - }, - onRequest: function(map) { - $.post({ - url: `/maps/${map.id}/access_request` - }) - GlobalUI.notifyUser('You will be notified by email if request accepted') - }, - onFollow: function(map) { - const isFollowing = map.isFollowedBy(Active.Mapper) - $.post({ - url: `/maps/${map.id}/${isFollowing ? 'un' : ''}follow` - }) - if (isFollowing) { - GlobalUI.notifyUser('You are no longer following this map') - Active.Mapper.unfollowMap(map.id) - } else { - GlobalUI.notifyUser('You are now following this map') - Active.Mapper.followMap(map.id) - } - self.render() - } - } - ReactDOM.render( - React.createElement(Maps, exploreObj), - document.getElementById('explore') - ).resize() - - if (cb) cb() + render: function() { + ReactApp.resize() + ReactApp.render() Loading.hide() }, loadMore: function() { @@ -85,14 +86,13 @@ const ExploreMaps = { } self.render() }, - handleSuccess: function(cb) { + handleSuccess: function() { var self = ExploreMaps self.pending = false if (self.collection && self.collection.id === 'mapper') { - self.fetchUserThenRender(cb) + self.fetchUserThenRender() } else { - self.render(cb) - Loading.hide() + self.render() } }, handleError: function() { @@ -103,8 +103,8 @@ const ExploreMaps = { var self = ExploreMaps if (self.mapper && self.mapper.id === self.collection.mapperId) { - self.render(cb) - return Loading.hide() + self.render() + return } // first load the mapper object and then call the render function @@ -112,14 +112,42 @@ const ExploreMaps = { url: '/users/' + self.collection.mapperId + '/details.json', success: function(response) { self.mapper = response - self.render(cb) - Loading.hide() + document.title = self.mapper.name + ' | Metamaps' + ReactApp.mobileTitle = self.mapper.name + self.render() }, error: function() { - self.render(cb) - Loading.hide() + self.render() } }) + }, + onStar: function(map) { + $.post('/maps/' + map.id + '/star') + map.set('star_count', map.get('star_count') + 1) + if (DataModel.Stars) DataModel.Stars.push({ user_id: Active.Mapper.id, map_id: map.id }) + DataModel.Maps.Starred.add(map) + GlobalUI.notifyUser('Map is now starred') + ReactApp.render() + }, + onRequest: function(map) { + $.post({ + url: `/maps/${map.id}/access_request` + }) + GlobalUI.notifyUser('You will be notified by email if request accepted') + }, + onMapFollow: function(map) { + const isFollowing = map.isFollowedBy(Active.Mapper) + $.post({ + url: `/maps/${map.id}/${isFollowing ? 'un' : ''}follow` + }) + if (isFollowing) { + GlobalUI.notifyUser('You are no longer following this map') + Active.Mapper.unfollowMap(map.id) + } else { + GlobalUI.notifyUser('You are now following this map') + Active.Mapper.followMap(map.id) + } + ReactApp.render() } } diff --git a/frontend/src/Metamaps/Views/TopicCard.js b/frontend/src/Metamaps/Views/TopicCard.js index 0b02fccd..a352cb44 100644 --- a/frontend/src/Metamaps/Views/TopicCard.js +++ b/frontend/src/Metamaps/Views/TopicCard.js @@ -5,69 +5,59 @@ import ReactDOM from 'react-dom' import Active from '../Active' import Visualize from '../Visualize' -import GlobalUI from '../GlobalUI' - -import ReactTopicCard from '../../components/TopicCard' +import GlobalUI, { ReactApp } from '../GlobalUI' const TopicCard = { - openTopicCard: null, // stores the topic that's currently open + openTopic: null, // stores the topic that's currently open metacodeSets: [], + redrawCanvas: () => { + Visualize.mGraph.plot() + }, init: function(serverData) { const self = TopicCard self.metacodeSets = serverData.metacodeSets }, - populateShowCard: function(topic) { + onTopicFollow: topic => { const self = TopicCard - ReactDOM.render( - React.createElement(ReactTopicCard, { - topic: topic, - ActiveMapper: Active.Mapper, - updateTopic: obj => { - topic.save(obj, { success: topic => self.populateShowCard(topic) }) - }, - onFollow: () => { - const isFollowing = topic.isFollowedBy(Active.Mapper) - $.post({ - url: `/topics/${topic.id}/${isFollowing ? 'un' : ''}follow` - }) - if (isFollowing) { - GlobalUI.notifyUser('You are no longer following this topic') - Active.Mapper.unfollowTopic(topic.id) - } else { - GlobalUI.notifyUser('You are now following this topic') - Active.Mapper.followTopic(topic.id) - } - self.populateShowCard(topic) - }, - metacodeSets: self.metacodeSets, - redrawCanvas: () => { - Visualize.mGraph.plot() - } - }), - document.getElementById('showcard') - ) - - // initialize draggability - $('.showcard').draggable({ - handle: '.metacodeImage', - stop: function() { - $(this).height('auto') - } + const isFollowing = topic.isFollowedBy(Active.Mapper) + $.post({ + url: `/topics/${topic.id}/${isFollowing ? 'un' : ''}follow` }) + if (isFollowing) { + GlobalUI.notifyUser('You are no longer following this topic') + Active.Mapper.unfollowTopic(topic.id) + } else { + GlobalUI.notifyUser('You are now following this topic') + Active.Mapper.followTopic(topic.id) + } + self.render() }, - showCard: function(node, opts) { + updateTopic: (topic, obj) => { + const self = TopicCard + topic.save(obj, { success: self.render }) + }, + render: function() { + ReactApp.render() + }, + showCard: function(node, opts = {}) { var self = TopicCard - if (!opts) opts = {} var topic = node.getData('topic') - self.openTopicCard = topic - // populate the card that's about to show with the right topics data - self.populateShowCard(topic) - return $('.showcard').fadeIn('fast', () => opts.complete && opts.complete()) + self.openTopic = topic + self.render() + $('.showcard').fadeIn('fast', () => { + $('.showcard').draggable({ + handle: '.metacodeImage', + stop: function() { + $(this).height('auto') + } + }) + opts.complete && opts.complete() + }) }, hideCard: function() { var self = TopicCard $('.showcard').fadeOut('fast') - self.openTopicCard = null + self.openTopic = null } } diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 43f6071b..aa0745da 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -8,7 +8,6 @@ import Active from './Active' import DataModel from './DataModel' import JIT from './JIT' import Loading from './Loading' -import Router from './Router' import TopicCard from './Views/TopicCard' const Visualize = { @@ -198,19 +197,6 @@ const Visualize = { } } hold() - - // update the url now that the map is ready - clearTimeout(Router.timeoutId) - Router.timeoutId = setTimeout(function() { - var m = Active.Map - var t = Active.Topic - - if (m && window.location.pathname !== '/maps/' + m.id) { - Router.navigateAndTrack('/maps/' + m.id) - } else if (t && window.location.pathname !== '/topics/' + t.id) { - Router.navigateAndTrack('/topics/' + t.id) - } - }, 800) }, clearVisualization: function() { Visualize.mGraph.graph.empty() diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index eb9969b0..12fcbe60 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -9,8 +9,7 @@ import DataModel from './DataModel' import Debug from './Debug' import Filter from './Filter' import GlobalUI, { - Search, CreateMap, ImportDialog, Account as GlobalUIAccount, - NotificationIcon + ReactApp, Search, CreateMap, ImportDialog } from './GlobalUI' import Import from './Import' import JIT from './JIT' @@ -18,12 +17,10 @@ import Listeners from './Listeners' import Loading from './Loading' import Map, { CheatSheet, InfoBox } from './Map' import Mapper from './Mapper' -import Mobile from './Mobile' import Mouse from './Mouse' import Organize from './Organize' import PasteInput from './PasteInput' import Realtime from './Realtime' -import Router from './Router' import Selected from './Selected' import Settings from './Settings' import Synapse from './Synapse' @@ -45,11 +42,10 @@ Metamaps.DataModel = DataModel Metamaps.Debug = Debug Metamaps.Filter = Filter Metamaps.GlobalUI = GlobalUI +Metamaps.GlobalUI.ReactApp = ReactApp Metamaps.GlobalUI.Search = Search Metamaps.GlobalUI.CreateMap = CreateMap -Metamaps.GlobalUI.Account = GlobalUIAccount Metamaps.GlobalUI.ImportDialog = ImportDialog -Metamaps.GlobalUI.NotificationIcon = NotificationIcon Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners @@ -59,12 +55,10 @@ Metamaps.Map.CheatSheet = CheatSheet Metamaps.Map.InfoBox = InfoBox Metamaps.Maps = {} Metamaps.Mapper = Mapper -Metamaps.Mobile = Mobile Metamaps.Mouse = Mouse Metamaps.Organize = Organize Metamaps.PasteInput = PasteInput Metamaps.Realtime = Realtime -Metamaps.Router = Router Metamaps.Selected = Selected Metamaps.Settings = Settings Metamaps.Synapse = Synapse @@ -86,26 +80,6 @@ document.addEventListener('DOMContentLoaded', function() { Metamaps[prop].init(Metamaps.ServerData) } } - // load whichever page you are on - if (Metamaps.currentSection === 'explore') { - const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - - Views.ExploreMaps.setCollection(DataModel.Maps[capitalize]) - if (Metamaps.currentPage === 'mapper') { - Views.ExploreMaps.fetchUserThenRender() - } else { - Views.ExploreMaps.render() - } - GlobalUI.showDiv('#explore') - } else if (Metamaps.currentSection === '' && Active.Mapper) { - Views.ExploreMaps.setCollection(DataModel.Maps.Active) - Views.ExploreMaps.render() - GlobalUI.showDiv('#explore') - } else if (Active.Map || Active.Topic) { - Loading.show() - JIT.prepareVizData() - GlobalUI.showDiv('#infovis') - } }) export default Metamaps diff --git a/frontend/src/components/App/AccountMenu.js b/frontend/src/components/App/AccountMenu.js new file mode 100644 index 00000000..99ab9301 --- /dev/null +++ b/frontend/src/components/App/AccountMenu.js @@ -0,0 +1,47 @@ +import React, { Component, PropTypes } from 'react' + +import onClickOutsideAddon from 'react-onclickoutside' + +class AccountMenu extends Component { + static propTypes = { + currentUser: PropTypes.object, + onInviteClick: PropTypes.func, + closeBox: PropTypes.func + } + + handleClickOutside = () => { + this.props.closeBox() + } + + render () { + const { currentUser, onInviteClick } = this.props + return
    + +

    {currentUser.get('name')}

    + +
    + } +} + +export default onClickOutsideAddon(AccountMenu) diff --git a/frontend/src/components/App/LoginForm.js b/frontend/src/components/App/LoginForm.js new file mode 100644 index 00000000..670b2bcb --- /dev/null +++ b/frontend/src/components/App/LoginForm.js @@ -0,0 +1,57 @@ +import React, { Component, PropTypes } from 'react' + +import onClickOutsideAddon from 'react-onclickoutside' + +class LoginForm extends Component { + static propTypes = { + loginFormAuthToken: PropTypes.string, + closeBox: PropTypes.func + } + + constructor(props) { + super(props) + this.state = { token: '' } + } + + componentDidMount() { + const token = document.head.getElementsByTagName('meta')['csrf-token'].content + this.setState({token}) + } + + emailInputDidMount(node) { + node.focus() + } + + handleClickOutside = () => { + this.props.closeBox() + } + + render () { + return
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    +
    + +
    + } +} + +export default onClickOutsideAddon(LoginForm) diff --git a/frontend/src/components/App/MobileHeader.js b/frontend/src/components/App/MobileHeader.js new file mode 100644 index 00000000..0c300248 --- /dev/null +++ b/frontend/src/components/App/MobileHeader.js @@ -0,0 +1,66 @@ +import React, { Component, PropTypes } from 'react' +import { Link } from 'react-router' + +class MobileHeader extends Component { + static propTypes = { + unreadNotificationsCount: PropTypes.number, + currentUser: PropTypes.object, + mobileTitle: PropTypes.string, + mobileTitleWidth: PropTypes.number, + onTitleClick: PropTypes.func + } + + constructor(props) { + super(props) + this.state = {open: false} + } + + toggle = () => { + this.setState({open: !this.state.open}) + } + + render() { + const { unreadNotificationsCount, currentUser, mobileTitle, mobileTitleWidth, onTitleClick } = this.props + const { open } = this.state + return
    +
    +
    + {mobileTitle} +
    + +
    + {open &&
    + {currentUser &&
      +
    • + + + {currentUser.get('name')} + +
    • +
    • New Map
    • +
    • My Maps
    • +
    • Shared With Me
    • +
    • Starred By Me
    • +
    • All Maps
    • +
    • Account
    • +
    • + Notifications + {unreadNotificationsCount > 0 &&
      } +
    • +
    • Sign Out
    • +
    } + {!currentUser && } +
    } +
    + } +} + +export default MobileHeader diff --git a/frontend/src/components/NotificationIcon.js b/frontend/src/components/App/NotificationIcon.js similarity index 90% rename from frontend/src/components/NotificationIcon.js rename to frontend/src/components/App/NotificationIcon.js index 201020b5..3f9c8980 100644 --- a/frontend/src/components/NotificationIcon.js +++ b/frontend/src/components/App/NotificationIcon.js @@ -1,6 +1,11 @@ import React, { PropTypes, Component } from 'react' class NotificationIcon extends Component { + + static propTypes = { + unreadNotificationsCount: PropTypes.number + } + constructor(props) { super(props) @@ -31,8 +36,4 @@ class NotificationIcon extends Component { } } -NotificationIcon.propTypes = { - unreadNotificationsCount: PropTypes.number -} - export default NotificationIcon diff --git a/frontend/src/components/App/Toast.js b/frontend/src/components/App/Toast.js new file mode 100644 index 00000000..4d27f007 --- /dev/null +++ b/frontend/src/components/App/Toast.js @@ -0,0 +1,15 @@ +import React, { Component, PropTypes } from 'react' + +class Toast extends Component { + static propTypes = { + message: PropTypes.string + } + + render () { + const { message } = this.props + const html = {__html: message} + return message ?

    : null + } +} + +export default Toast diff --git a/frontend/src/components/App/UpperLeftUI.js b/frontend/src/components/App/UpperLeftUI.js new file mode 100644 index 00000000..427e63d8 --- /dev/null +++ b/frontend/src/components/App/UpperLeftUI.js @@ -0,0 +1,38 @@ +import React, { Component, PropTypes } from 'react' +import { Link } from 'react-router' + +class UpperLeftUI extends Component { + static propTypes = { + currentUser: PropTypes.object, + map: PropTypes.object, + userRequested: PropTypes.bool, + requestAnswered: PropTypes.bool, + requestApproved: PropTypes.bool, + onRequestClick: PropTypes.func + } + + render () { + const { map, currentUser, userRequested, requestAnswered, requestApproved, onRequestClick } = this.props + return

    +
    + {currentUser && METAMAPS} + {!currentUser && METAMAPS} +
    +
    + +
    +
    +
    +
    + {map && !map.authorizeToEdit(currentUser) &&
    +
    View Only
    + {currentUser && !userRequested &&
    Request Access
    } + {userRequested && !requestAnswered &&
    Request Pending
    } + {userRequested && requestAnswered && !requestApproved &&
    Request Not Accepted
    } +
    } +
    +
    + } +} + +export default UpperLeftUI diff --git a/frontend/src/components/App/UpperRightUI.js b/frontend/src/components/App/UpperRightUI.js new file mode 100644 index 00000000..e8494b54 --- /dev/null +++ b/frontend/src/components/App/UpperRightUI.js @@ -0,0 +1,58 @@ +import React, { Component, PropTypes } from 'react' + +import AccountMenu from './AccountMenu' +import LoginForm from './LoginForm' +import NotificationIcon from './NotificationIcon' + +class UpperRightUI extends Component { + static propTypes = { + currentUser: PropTypes.object, + signInPage: PropTypes.bool, + unreadNotificationsCount: PropTypes.number, + openInviteLightbox: PropTypes.func + } + + constructor(props) { + super(props) + this.state = {accountBoxOpen: false} + } + + reset = () => { + this.setState({accountBoxOpen: false}) + } + + toggleAccountBox = () => { + this.setState({accountBoxOpen: !this.state.accountBoxOpen}) + } + + render () { + const { currentUser, signInPage, unreadNotificationsCount, openInviteLightbox } = this.props + const { accountBoxOpen } = this.state + return
    + {currentUser && +
    + Create New Map +
    +
    } + {currentUser && + + } + {!signInPage &&
    +
    +
    Account
    + {currentUser && } + {!currentUser && 'SIGN IN'} + {!currentUser &&
    } +
    + {accountBoxOpen &&
    + {currentUser + ? + : } +
    } +
    } +
    +
    + } +} + +export default UpperRightUI diff --git a/frontend/src/components/App/index.js b/frontend/src/components/App/index.js new file mode 100644 index 00000000..22d327a2 --- /dev/null +++ b/frontend/src/components/App/index.js @@ -0,0 +1,67 @@ +import React, { Component, PropTypes } from 'react' + +import MobileHeader from './MobileHeader' +import UpperLeftUI from './UpperLeftUI' +import UpperRightUI from './UpperRightUI' +import Toast from './Toast' + +class App extends Component { + static propTypes = { + children: PropTypes.object, + toast: PropTypes.string, + unreadNotificationsCount: PropTypes.number, + location: PropTypes.object, + mobile: PropTypes.bool, + mobileTitle: PropTypes.string, + mobileTitleWidth: PropTypes.number, + mobileTitleClick: PropTypes.func, + openInviteLightbox: PropTypes.func, + map: PropTypes.object, + userRequested: PropTypes.bool, + requestAnswered: PropTypes.bool, + requestApproved: PropTypes.bool, + onRequestAccess: PropTypes.func + } + + static childContextTypes = { + location: PropTypes.object + } + + getChildContext () { + const { location } = this.props + return {location} + } + + render () { + const { children, toast, unreadNotificationsCount, openInviteLightbox, + mobile, mobileTitle, mobileTitleWidth, mobileTitleClick, location, + map, userRequested, requestAnswered, requestApproved, + onRequestAccess } = this.props + const { pathname } = location || {} + // this fixes a bug that happens otherwise when you logout + const currentUser = this.props.currentUser && this.props.currentUser.id ? this.props.currentUser : null + const unauthedHome = pathname === '/' && !currentUser + return
    + {mobile && } + {!unauthedHome && } + {!mobile && } + + {!mobile && currentUser && } + {children} +
    + } +} + +export default App diff --git a/frontend/src/components/MapChat/index.js b/frontend/src/components/MapChat/index.js deleted file mode 100644 index 27bff527..00000000 --- a/frontend/src/components/MapChat/index.js +++ /dev/null @@ -1,193 +0,0 @@ -import React, { PropTypes, Component } from 'react' -import Unread from './Unread' -import Participant from './Participant' -import Message from './Message' -import NewMessage from './NewMessage' -import Util from '../../Metamaps/Util' - -function makeList(messages) { - let currentHeader - return messages ? messages.map(m => { - let heading = false - if (!currentHeader) { - heading = true - currentHeader = m - } else { - // not same user or time diff of greater than 3 minutes - heading = m.user_id !== currentHeader.user_id || Math.floor(Math.abs(new Date(currentHeader.created_at) - new Date(m.created_at)) / 60000) > 3 - currentHeader = heading ? m : currentHeader - } - return - }) : null -} - -class MapChat extends Component { - constructor(props) { - super(props) - - this.state = { - unreadMessages: 0, - open: false, - messageText: '', - alertSound: true, // whether to play sounds on arrival of new messages or not - cursorsShowing: true, - videosShowing: true - } - } - - reset = () => { - this.setState({ - unreadMessages: 0, - open: false, - messageText: '', - alertSound: true, // whether to play sounds on arrival of new messages or not - cursorsShowing: true, - videosShowing: true - }) - } - - close = () => { - this.setState({open: false}) - this.props.onClose() - this.messageInput.blur() - } - - open = () => { - this.scroll() - this.setState({open: true, unreadMessages: 0}) - this.props.onOpen() - this.messageInput.focus() - } - - newMessage = () => { - if (!this.state.open) this.setState({unreadMessages: this.state.unreadMessages + 1}) - } - - scroll = () => { - this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight - } - - toggleDrawer = () => { - if (this.state.open) this.close() - else if (!this.state.open) this.open() - } - - toggleAlertSound = () => { - this.setState({alertSound: !this.state.alertSound}) - this.props.soundToggleClick() - } - - toggleCursorsShowing = () => { - this.setState({cursorsShowing: !this.state.cursorsShowing}) - this.props.cursorToggleClick() - } - - toggleVideosShowing = () => { - this.setState({videosShowing: !this.state.videosShowing}) - this.props.videoToggleClick() - } - - handleChange = key => e => { - this.setState({ - [key]: e.target.value - }) - } - - handleTextareaKeyUp = e => { - if (e.which === 13) { - e.preventDefault() - const text = Util.removeEmoji(this.state.messageText) - this.props.handleInputMessage(text) - this.setState({ messageText: '' }) - } - } - - focusMessageInput = () => { - if (!this.messageInput) return - this.messageInput.focus() - } - - render = () => { - const rightOffset = this.state.open ? '0' : '-300px' - const { conversationLive, isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props - const { videosShowing, cursorsShowing, alertSound, unreadMessages } = this.state - return ( -
    -
    - PARTICIPANTS -
    -
    -
    -
    - {conversationLive &&
    - LIVE - {isParticipating && - LEAVE - } - {!isParticipating && - JOIN - } -
    } - {participants.map(participant => - )} -
    -
    - CHAT -
    -
    -
    -
    Chat
    - -
    -
    { this.messagesDiv = div }}> - {makeList(messages)} -
    - { this.messageInput = textarea }, - placeholder: 'Send a message...', - onKeyUp: this.handleTextareaKeyUp, - onFocus: this.props.inputFocus, - onBlur: this.props.inputBlur - }} - /> -
    - ) - } -} - -MapChat.propTypes = { - conversationLive: PropTypes.bool, - isParticipating: PropTypes.bool, - onOpen: PropTypes.func, - onClose: PropTypes.func, - leaveCall: PropTypes.func, - joinCall: PropTypes.func, - inviteACall: PropTypes.func, - inviteToJoin: PropTypes.func, - videoToggleClick: PropTypes.func, - cursorToggleClick: PropTypes.func, - soundToggleClick: PropTypes.func, - participants: PropTypes.arrayOf(PropTypes.shape({ - color: PropTypes.string, // css color - id: PropTypes.number, - image: PropTypes.string, // image url - self: PropTypes.bool, - username: PropTypes.string, - isParticipating: PropTypes.bool, - isPending: PropTypes.bool - })) -} - -export default MapChat diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/MapView/ImportDialogBox.js similarity index 100% rename from frontend/src/components/ImportDialogBox.js rename to frontend/src/components/MapView/ImportDialogBox.js diff --git a/frontend/src/components/MapView/Instructions.js b/frontend/src/components/MapView/Instructions.js new file mode 100644 index 00000000..66331fa1 --- /dev/null +++ b/frontend/src/components/MapView/Instructions.js @@ -0,0 +1,23 @@ +import React, { Component, PropTypes } from 'react' + +class Instructions extends Component { + + static propTypes = { + mobile: PropTypes.bool, + hasLearnedTopicCreation: PropTypes.bool + } + + render() { + const { hasLearnedTopicCreation, mobile } = this.props + return hasLearnedTopicCreation ? null :
    + {!mobile &&
    + Double-click to
    add a topic +
    } + {mobile &&
    + Double-tap to
    add a topic +
    } +
    + } +} + +export default Instructions diff --git a/frontend/src/components/MapChat/Message.js b/frontend/src/components/MapView/MapChat/Message.js similarity index 96% rename from frontend/src/components/MapChat/Message.js rename to frontend/src/components/MapView/MapChat/Message.js index 7cfe9ff2..e4caf3ce 100644 --- a/frontend/src/components/MapChat/Message.js +++ b/frontend/src/components/MapView/MapChat/Message.js @@ -1,6 +1,6 @@ import React from 'react' import Autolinker from 'autolinker' -import Util from '../../Metamaps/Util' +import Util from '../../../Metamaps/Util' const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false }) diff --git a/frontend/src/components/MapChat/NewMessage.js b/frontend/src/components/MapView/MapChat/NewMessage.js similarity index 100% rename from frontend/src/components/MapChat/NewMessage.js rename to frontend/src/components/MapView/MapChat/NewMessage.js diff --git a/frontend/src/components/MapChat/Participant.js b/frontend/src/components/MapView/MapChat/Participant.js similarity index 100% rename from frontend/src/components/MapChat/Participant.js rename to frontend/src/components/MapView/MapChat/Participant.js diff --git a/frontend/src/components/MapChat/Unread.js b/frontend/src/components/MapView/MapChat/Unread.js similarity index 100% rename from frontend/src/components/MapChat/Unread.js rename to frontend/src/components/MapView/MapChat/Unread.js diff --git a/frontend/src/components/MapView/MapChat/index.js b/frontend/src/components/MapView/MapChat/index.js new file mode 100644 index 00000000..25eb8e41 --- /dev/null +++ b/frontend/src/components/MapView/MapChat/index.js @@ -0,0 +1,188 @@ +import React, { PropTypes, Component } from 'react' +import Unread from './Unread' +import Participant from './Participant' +import Message from './Message' +import NewMessage from './NewMessage' +import Util from '../../../Metamaps/Util' + +function makeList(messages) { + let currentHeader + return messages ? messages.map(m => { + let heading = false + if (!currentHeader) { + heading = true + currentHeader = m + } else { + // not same user or time diff of greater than 3 minutes + heading = m.user_id !== currentHeader.user_id || Math.floor(Math.abs(new Date(currentHeader.created_at) - new Date(m.created_at)) / 60000) > 3 + currentHeader = heading ? m : currentHeader + } + return + }) : null +} + +class MapChat extends Component { + constructor(props) { + super(props) + + this.state = { + messageText: '', + alertSound: true, // whether to play sounds on arrival of new messages or not + cursorsShowing: true, + videosShowing: true + } + } + + componentDidUpdate(prevProps) { + const { messages } = this.props + const prevMessages = prevProps.messages + if (messages.length !== prevMessages.length) setTimeout(() => this.scroll(), 50) + } + + reset = () => { + this.setState({ + messageText: '', + alertSound: true, // whether to play sounds on arrival of new messages or not + cursorsShowing: true, + videosShowing: true + }) + } + + close = () => { + this.props.onClose() + } + + open = () => { + this.props.onOpen() + setTimeout(() => this.scroll(), 50) + } + + scroll = () => { + // hack: figure out how to do this right + this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight + 100 + } + + toggleDrawer = () => { + if (this.props.chatOpen) this.close() + else if (!this.props.chatOpen) this.open() + } + + toggleAlertSound = () => { + this.setState({alertSound: !this.state.alertSound}) + this.props.soundToggleClick() + } + + toggleCursorsShowing = () => { + this.setState({cursorsShowing: !this.state.cursorsShowing}) + this.props.cursorToggleClick() + } + + toggleVideosShowing = () => { + this.setState({videosShowing: !this.state.videosShowing}) + this.props.videoToggleClick() + } + + handleChange = key => e => { + this.setState({ + [key]: e.target.value + }) + } + + handleTextareaKeyUp = e => { + if (e.which === 13) { + e.preventDefault() + const text = Util.removeEmoji(this.state.messageText) + this.props.handleInputMessage(text) + this.setState({ messageText: '' }) + } + } + + render = () => { + const { unreadMessages, chatOpen, conversationLive, + isParticipating, participants, messages, inviteACall, inviteToJoin } = this.props + const { videosShowing, cursorsShowing, alertSound } = this.state + const rightOffset = chatOpen ? '0' : '-300px' + return ( +
    +
    +
    + PARTICIPANTS +
    +
    +
    +
    + {conversationLive &&
    + LIVE + {isParticipating && + LEAVE + } + {!isParticipating && + JOIN + } +
    } + {participants.map(participant => + )} +
    +
    + CHAT +
    +
    +
    +
    Chat
    + +
    +
    { this.messagesDiv = div }}> + {makeList(messages)} +
    + {chatOpen && { textarea && textarea.focus() }, + placeholder: 'Send a message...', + onKeyUp: this.handleTextareaKeyUp, + onFocus: this.props.inputFocus, + onBlur: this.props.inputBlur + }} + />} +
    +
    + ) + } +} + +MapChat.propTypes = { + unreadMessages: PropTypes.number, + chatOpen: PropTypes.bool, + conversationLive: PropTypes.bool, + isParticipating: PropTypes.bool, + onOpen: PropTypes.func, + onClose: PropTypes.func, + leaveCall: PropTypes.func, + joinCall: PropTypes.func, + inviteACall: PropTypes.func, + inviteToJoin: PropTypes.func, + videoToggleClick: PropTypes.func, + cursorToggleClick: PropTypes.func, + soundToggleClick: PropTypes.func, + participants: PropTypes.arrayOf(PropTypes.shape({ + color: PropTypes.string, // css color + id: PropTypes.number, + image: PropTypes.string, // image url + self: PropTypes.bool, + username: PropTypes.string, + isParticipating: PropTypes.bool, + isPending: PropTypes.bool + })) +} + +export default MapChat diff --git a/frontend/src/components/MapView/MapInfoBox.js b/frontend/src/components/MapView/MapInfoBox.js new file mode 100644 index 00000000..7f64da0f --- /dev/null +++ b/frontend/src/components/MapView/MapInfoBox.js @@ -0,0 +1,23 @@ +import React, { Component, PropTypes } from 'react' + +class MapInfoBox extends Component { + static propTypes = { + currentUser: PropTypes.object, + map: PropTypes.object, + infoBoxHtml: PropTypes.string + } + + render () { + const { currentUser, map, infoBoxHtml } = this.props + if (!map) return null + const html = {__html: infoBoxHtml} + const isCreator = map.authorizePermissionChange(currentUser) + const canEdit = map.authorizeToEdit(currentUser) + let classes = 'mapInfoBox mapElement mapElementHidden permission ' + classes += isCreator ? 'yourMap' : '' + classes += canEdit ? ' canEdit' : '' + return
    + } +} + +export default MapInfoBox diff --git a/frontend/src/components/MapView/index.js b/frontend/src/components/MapView/index.js new file mode 100644 index 00000000..21932bfd --- /dev/null +++ b/frontend/src/components/MapView/index.js @@ -0,0 +1,127 @@ +import React, { Component, PropTypes } from 'react' + +import DataVis from '../common/DataVis' +import UpperOptions from '../common/UpperOptions' +import InfoAndHelp from '../common/InfoAndHelp' +import Instructions from './Instructions' +import VisualizationControls from '../common/VisualizationControls' +import MapChat from './MapChat' +import TopicCard from '../TopicCard' + +export default class MapView extends Component { + + static propTypes = { + mobile: PropTypes.bool, + mapId: PropTypes.string, + map: PropTypes.object, + mapIsStarred: PropTypes.bool, + onMapStar: PropTypes.func, + onMapUnstar: PropTypes.func, + filterData: PropTypes.object, + allForFiltering: PropTypes.object, + visibleForFiltering: PropTypes.object, + toggleMetacode: PropTypes.func, + toggleMapper: PropTypes.func, + toggleSynapse: PropTypes.func, + filterAllMetacodes: PropTypes.func, + filterAllMappers: PropTypes.func, + filterAllSynapses: PropTypes.func, + toggleMapInfoBox: PropTypes.func, + infoBoxHtml: PropTypes.string, + currentUser: PropTypes.object, + endActiveMap: PropTypes.func, + launchNewMap: PropTypes.func, + openImportLightbox: PropTypes.func, + forkMap: PropTypes.func, + openHelpLightbox: PropTypes.func, + onZoomExtents: PropTypes.func, + onZoomIn: PropTypes.func, + onZoomOut: PropTypes.func, + hasLearnedTopicCreation: PropTypes.bool + } + + constructor(props) { + super(props) + this.state = { + chatOpen: false + } + } + + componentWillUnmount() { + this.endMap() + } + + endMap() { + this.setState({ + chatOpen: false + }) + this.mapChat.reset() + this.upperOptions.reset() + this.props.endActiveMap() + } + + componentDidUpdate(prevProps) { + const oldMapId = prevProps.mapId + const { mapId, launchNewMap } = this.props + if (!oldMapId && mapId) launchNewMap(mapId) + else if (oldMapId && mapId && oldMapId !== mapId) { + this.endMap() + launchNewMap(mapId) + } + else if (oldMapId && !mapId) this.endMap() + } + + render = () => { + const { mobile, map, currentUser, onOpen, onClose, + toggleMapInfoBox, infoBoxHtml, allForFiltering, visibleForFiltering, + toggleMetacode, toggleMapper, toggleSynapse, filterAllMetacodes, + filterAllMappers, filterAllSynapses, filterData, + openImportLightbox, forkMap, openHelpLightbox, + mapIsStarred, onMapStar, onMapUnstar, + onZoomExtents, onZoomIn, onZoomOut, hasLearnedTopicCreation } = this.props + const { chatOpen } = this.state + const onChatOpen = () => { + this.setState({chatOpen: true}) + onOpen() + } + const onChatClose = () => { + this.setState({chatOpen: false}) + onClose() + } + const canEditMap = map && map.authorizeToEdit(currentUser) + // TODO: stop using {...this.props} and make explicit + return
    + this.upperOptions = x} + map={map} + currentUser={currentUser} + onImportClick={openImportLightbox} + onForkClick={forkMap} + canEditMap={canEditMap} + filterData={filterData} + allForFiltering={allForFiltering} + visibleForFiltering={visibleForFiltering} + toggleMetacode={toggleMetacode} + toggleMapper={toggleMapper} + toggleSynapse={toggleSynapse} + filterAllMetacodes={filterAllMetacodes} + filterAllMappers={filterAllMappers} + filterAllSynapses={filterAllSynapses} /> + + + {currentUser && } + {currentUser && this.mapChat = x} />} + + +
    + } +} diff --git a/frontend/src/components/Maps/Header.js b/frontend/src/components/Maps/Header.js index 9de1eb9b..39fa6c62 100644 --- a/frontend/src/components/Maps/Header.js +++ b/frontend/src/components/Maps/Header.js @@ -1,4 +1,5 @@ import React, { Component, PropTypes } from 'react' +import { Link } from 'react-router' import _ from 'lodash' const MapLink = props => { @@ -9,16 +10,16 @@ const MapLink = props => { } return ( - +
    {text} -
    + ) } class Header extends Component { render = () => { - const { signedIn, section } = this.props + const { signedIn, section, user } = this.props const activeClass = (title) => { let forClass = 'exploreMapsButton' @@ -39,38 +40,33 @@ class Header extends Component { {mapper ? (
    - -
    {this.props.user.name}’s Maps
    + {user && } + {user &&
    {user.name}’s Maps
    }
    ) : null } diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/components/Maps/MapCard.js index 4dd9ea18..bd1b137c 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -1,4 +1,5 @@ import React, { Component, PropTypes } from 'react' +import { Link } from 'react-router' import { find, values } from 'lodash' import Util from '../../Metamaps/Util' @@ -24,7 +25,7 @@ class Menu extends Component { } render = () => { - const { currentUser, map, onStar, onRequest, onFollow } = this.props + const { currentUser, map, onStar, onRequest, onMapFollow } = this.props const isFollowing = map.isFollowedBy(currentUser) const style = { display: this.state.open ? 'block' : 'none' } @@ -37,7 +38,7 @@ class Menu extends Component {
    • { this.toggle() && onStar(map) }}>Star Map
    • { !map.authorizeToEdit(currentUser) &&
    • { this.toggle() && onRequest(map) }}>Request Access
    • } - { Util.isTester(currentUser) &&
    • { this.toggle() && onFollow(map) }}>{isFollowing ? 'Unfollow' : 'Follow'}
    • } + { Util.isTester(currentUser) &&
    • { this.toggle() && onMapFollow(map) }}>{isFollowing ? 'Unfollow' : 'Follow'}
    • }
    } @@ -47,7 +48,7 @@ Menu.propTypes = { map: PropTypes.object.isRequired, onStar: PropTypes.func.isRequired, onRequest: PropTypes.func.isRequired, - onFollow: PropTypes.func.isRequired + onMapFollow: PropTypes.func.isRequired } const Metadata = (props) => { @@ -78,13 +79,13 @@ const Metadata = (props) => { } const checkAndWrapInA = (shouldWrap, classString, mapId, element) => { - if (shouldWrap) return { element } + if (shouldWrap) return { element } else return element } class MapCard extends Component { render = () => { - const { map, mobile, juntoState, currentUser, onRequest, onStar, onFollow } = this.props + const { map, mobile, juntoState, currentUser, onRequest, onStar, onMapFollow } = this.props const hasMap = (juntoState.liveMaps[map.id] && values(juntoState.liveMaps[map.id]).length) || null const realtimeMap = juntoState.liveMaps[map.id] @@ -135,7 +136,7 @@ class MapCard extends Component {
    ) } { !mobile && hasMapper &&
    } { !mobile && hasConversation &&
    } - { !mobile && currentUser && } + { !mobile && currentUser && }
    ) }
    @@ -150,7 +151,7 @@ MapCard.propTypes = { currentUser: PropTypes.object, onStar: PropTypes.func.isRequired, onRequest: PropTypes.func.isRequired, - onFollow: PropTypes.func.isRequired + onMapFollow: PropTypes.func.isRequired } export default MapCard diff --git a/frontend/src/components/Maps/index.js b/frontend/src/components/Maps/index.js index 18d0fe92..6412a33c 100644 --- a/frontend/src/components/Maps/index.js +++ b/frontend/src/components/Maps/index.js @@ -4,59 +4,56 @@ import Header from './Header' import MapperCard from './MapperCard' import MapCard from './MapCard' -// 220 wide + 16 padding on both sides -const MAP_WIDTH = 252 -const MOBILE_VIEW_BREAKPOINT = 504 -const MOBILE_VIEW_PADDING = 40 -const MAX_COLUMNS = 4 - class Maps extends Component { - constructor(props) { - super(props) - this.state = { mapsWidth: 0 } + static propTypes = { + section: PropTypes.string, + maps: PropTypes.object, + juntoState: PropTypes.object, + moreToLoad: PropTypes.bool, + user: PropTypes.object, + currentUser: PropTypes.object, + loadMore: PropTypes.func, + pending: PropTypes.bool, + onStar: PropTypes.func, + onRequest: PropTypes.func, + onMapFollow: PropTypes.func, + mapsWidth: PropTypes.number, + mobile: PropTypes.bool } - componentDidMount() { - window && window.addEventListener('resize', this.resize) - this.refs.maps.addEventListener('scroll', throttle(this.scroll, 500, { leading: true, trailing: false })) - this.resize() + static contextTypes = { + location: PropTypes.object } - componentWillUnmount() { - window && window.removeEventListener('resize', this.resize) - } - - resize = () => { - const { maps, user, currentUser } = this.props - const numCards = maps.length + (user || currentUser ? 1 : 0) - const mapSpaces = Math.floor(document.body.clientWidth / MAP_WIDTH) - const mapsWidth = document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT - ? document.body.clientWidth - MOBILE_VIEW_PADDING - : Math.min(MAX_COLUMNS, Math.min(numCards, mapSpaces)) * MAP_WIDTH - this.setState({ mapsWidth }) + mapsDidMount = (node) => { + if (node) { + this.mapsDiv = node + node.addEventListener('scroll', throttle(this.scroll, 500, { leading: true, trailing: false })) + } } scroll = () => { const { loadMore, moreToLoad, pending } = this.props - const { maps } = this.refs - if (moreToLoad && !pending && maps.scrollTop + maps.offsetHeight > maps.scrollHeight - 300) { + const { mapsDiv } = this + if (moreToLoad && !pending && mapsDiv.scrollTop + mapsDiv.offsetHeight > mapsDiv.scrollHeight - 300) { loadMore() } } render = () => { - const { maps, currentUser, juntoState, pending, section, user, onStar, onRequest, onFollow } = this.props - const style = { width: this.state.mapsWidth + 'px' } - const mobile = document && document.body.clientWidth <= MOBILE_VIEW_BREAKPOINT + const { mobile, maps, mapsWidth, currentUser, juntoState, pending, section, user, onStar, onRequest, onMapFollow } = this.props + const style = { width: mapsWidth + 'px' } + + if (!maps) return null // do loading here instead return (
    -
    +
    { user ? : null } { currentUser && !user && !(pending && maps.length === 0) ? : null } - { maps.models.map(map => ) } + { maps.models.map(map => ) }
    @@ -69,18 +66,4 @@ class Maps extends Component { } } -Maps.propTypes = { - section: PropTypes.string.isRequired, - maps: PropTypes.object.isRequired, - juntoState: PropTypes.object.isRequired, - moreToLoad: PropTypes.bool.isRequired, - user: PropTypes.object, - currentUser: PropTypes.object, - loadMore: PropTypes.func, - pending: PropTypes.bool.isRequired, - onStar: PropTypes.func.isRequired, - onRequest: PropTypes.func.isRequired, - onFollow: PropTypes.func.isRequired -} - export default Maps diff --git a/frontend/src/components/TopicCard/Follow.js b/frontend/src/components/TopicCard/Follow.js index 786001d7..653d8b18 100644 --- a/frontend/src/components/TopicCard/Follow.js +++ b/frontend/src/components/TopicCard/Follow.js @@ -2,8 +2,8 @@ import React, { PropTypes, Component } from 'react' class Follow extends Component { render = () => { - const { isFollowing, onFollow } = this.props - return
    + const { isFollowing, onTopicFollow } = this.props + return
    {isFollowing ? 'Unfollow' : 'Follow'}
    } @@ -11,7 +11,7 @@ class Follow extends Component { Follow.propTypes = { isFollowing: PropTypes.bool, - onFollow: PropTypes.func + onTopicFollow: PropTypes.func } export default Follow diff --git a/frontend/src/components/TopicCard/Links.js b/frontend/src/components/TopicCard/Links.js index ad1758d3..401fb565 100644 --- a/frontend/src/components/TopicCard/Links.js +++ b/frontend/src/components/TopicCard/Links.js @@ -1,6 +1,7 @@ /* global $ */ import React, { PropTypes, Component } from 'react' +import { Link } from 'react-router' import MetacodeSelect from '../MetacodeSelect' import Permission from './Permission' @@ -52,21 +53,21 @@ class Links extends Component { } let output = [] - + firstFiveLinks.forEach(obj => { - output.push(
  • {obj.mapName}
  • ) + output.push(
  • {obj.mapName}
  • ) }) if (extraLinks.length > 0) { if (this.state.showMoreMaps) { extraLinks.forEach(obj => { - output.push(
  • {obj.mapName}
  • ) + output.push(
  • {obj.mapName}
  • ) }) } const text = this.state.showMoreMaps ? 'See less...' : `See ${extraLinks.length} more...` output.push(
  • {text}
  • ) } - + return output } diff --git a/frontend/src/components/TopicCard/index.js b/frontend/src/components/TopicCard/index.js index 2c30d45e..7ef73673 100644 --- a/frontend/src/components/TopicCard/index.js +++ b/frontend/src/components/TopicCard/index.js @@ -7,12 +7,18 @@ import Attachments from './Attachments' import Follow from './Follow' import Util from '../../Metamaps/Util' - class ReactTopicCard extends Component { render = () => { - const { topic, ActiveMapper, onFollow } = this.props - const authorizedToEdit = topic.authorizeToEdit(ActiveMapper) - const isFollowing = topic.isFollowedBy(ActiveMapper) + const { currentUser, onTopicFollow, updateTopic } = this.props + const topic = this.props.openTopic + + if (!topic) return null + + const wrappedUpdateTopic = obj => updateTopic(topic, obj) + const wrappedTopicFollow = () => onTopicFollow(topic) + + const authorizedToEdit = topic.authorizeToEdit(currentUser) + const isFollowing = topic.isFollowedBy(currentUser) const hasAttachment = topic.get('link') && topic.get('link') !== '' let classname = 'permission' @@ -21,31 +27,33 @@ class ReactTopicCard extends Component { } else { classname += ' cannotEdit' } - if (topic.authorizePermissionChange(ActiveMapper)) classname += ' yourTopic' + if (topic.authorizePermissionChange(currentUser)) classname += ' yourTopic' return ( -
    -
    - - <Links topic={topic} - ActiveMapper={this.props.ActiveMapper} - updateTopic={this.props.updateTopic} - metacodeSets={this.props.metacodeSets} - redrawCanvas={this.props.redrawCanvas} - /> - <Desc desc={topic.get('desc')} - authorizedToEdit={authorizedToEdit} - onChange={this.props.updateTopic} - /> - <Attachments topic={topic} - authorizedToEdit={authorizedToEdit} - updateTopic={this.props.updateTopic} - /> - {Util.isTester(ActiveMapper) && <Follow isFollowing={isFollowing} onFollow={onFollow} />} - <div className="clearfloat"></div> + <div className="showcard mapElement mapElementHidden" id="showcard"> + <div className={classname}> + <div className={`CardOnGraph ${hasAttachment ? 'hasAttachment' : ''}`} id={`topic_${topic.id}`}> + <Title name={topic.get('name')} + authorizedToEdit={authorizedToEdit} + onChange={wrappedUpdateTopic} + /> + <Links topic={topic} + ActiveMapper={this.props.currentUser} + updateTopic={wrappedUpdateTopic} + metacodeSets={this.props.metacodeSets} + redrawCanvas={this.props.redrawCanvas} + /> + <Desc desc={topic.get('desc')} + authorizedToEdit={authorizedToEdit} + onChange={wrappedUpdateTopic} + /> + <Attachments topic={topic} + authorizedToEdit={authorizedToEdit} + updateTopic={wrappedUpdateTopic} + /> + {Util.isTester(currentUser) && <Follow isFollowing={isFollowing} onTopicFollow={wrappedTopicFollow} />} + <div className="clearfloat"></div> + </div> </div> </div> ) @@ -53,10 +61,10 @@ class ReactTopicCard extends Component { } ReactTopicCard.propTypes = { - topic: PropTypes.object, - ActiveMapper: PropTypes.object, + openTopic: PropTypes.object, + currentUser: PropTypes.object, updateTopic: PropTypes.func, - onFollow: PropTypes.func, + onTopicFollow: PropTypes.func, metacodeSets: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string, metacodes: PropTypes.arrayOf(PropTypes.shape({ diff --git a/frontend/src/components/TopicView/index.js b/frontend/src/components/TopicView/index.js new file mode 100644 index 00000000..edd7b92e --- /dev/null +++ b/frontend/src/components/TopicView/index.js @@ -0,0 +1,81 @@ +import React, { Component, PropTypes } from 'react' + +import DataVis from '../common/DataVis' +import UpperOptions from '../common/UpperOptions' +import InfoAndHelp from '../common/InfoAndHelp' +import VisualizationControls from '../common/VisualizationControls' +import TopicCard from '../TopicCard' + +export default class TopicView extends Component { + + static propTypes = { + mobile: PropTypes.bool, + topicId: PropTypes.string, + topic: PropTypes.object, + filterData: PropTypes.object, + allForFiltering: PropTypes.object, + visibleForFiltering: PropTypes.object, + toggleMetacode: PropTypes.func, + toggleMapper: PropTypes.func, + toggleSynapse: PropTypes.func, + filterAllMetacodes: PropTypes.func, + filterAllMappers: PropTypes.func, + filterAllSynapses: PropTypes.func, + currentUser: PropTypes.object, + endActiveTopic: PropTypes.func, + launchNewTopic: PropTypes.func, + openHelpLightbox: PropTypes.func, + forkMap: PropTypes.func, + onZoomIn: PropTypes.func, + onZoomOut: PropTypes.func + } + + componentWillUnmount() { + this.endTopic() + } + + endTopic() { + this.upperOptions.reset() + this.props.endActiveTopic() + } + + componentDidUpdate(prevProps) { + const oldTopicId = prevProps.topicId + const { topicId, launchNewTopic } = this.props + if (!oldTopicId && topicId) launchNewTopic(topicId) + else if (oldTopicId && topicId && oldTopicId !== topicId) { + this.endTopic() + launchNewTopic(topicId) + } + else if (oldTopicId && !topicId) this.endTopic() + } + + render = () => { + const { mobile, topic, currentUser, allForFiltering, visibleForFiltering, + toggleMetacode, toggleMapper, toggleSynapse, filterAllMetacodes, + filterAllMappers, filterAllSynapses, filterData, forkMap, + openHelpLightbox, onZoomIn, onZoomOut } = this.props + // TODO: stop using {...this.props} and make explicit + return <div className="topicWrapper"> + <UpperOptions ref={x => this.upperOptions = x} + currentUser={currentUser} + topic={topic} + onForkClick={forkMap} + filterData={filterData} + allForFiltering={allForFiltering} + visibleForFiltering={visibleForFiltering} + toggleMetacode={toggleMetacode} + toggleMapper={toggleMapper} + toggleSynapse={toggleSynapse} + filterAllMetacodes={filterAllMetacodes} + filterAllMappers={filterAllMappers} + filterAllSynapses={filterAllSynapses} /> + <DataVis /> + <TopicCard {...this.props} /> + <VisualizationControls onClickZoomIn={onZoomIn} + onClickZoomOut={onZoomOut} /> + <InfoAndHelp topic={topic} + onHelpClick={openHelpLightbox} /> + </div> + } +} diff --git a/frontend/src/components/common/DataVis.js b/frontend/src/components/common/DataVis.js new file mode 100644 index 00000000..f5f370d4 --- /dev/null +++ b/frontend/src/components/common/DataVis.js @@ -0,0 +1,12 @@ +import React, { Component, PropTypes } from 'react' + +class DataVis extends Component { + static propTypes = { + } + + render () { + return <div id="infovis" /> + } +} + +export default DataVis diff --git a/frontend/src/components/common/FilterBox.js b/frontend/src/components/common/FilterBox.js new file mode 100644 index 00000000..01343d3c --- /dev/null +++ b/frontend/src/components/common/FilterBox.js @@ -0,0 +1,118 @@ +import React, { Component, PropTypes } from 'react' + +import onClickOutsideAddon from 'react-onclickoutside' + +class FilterBox extends Component { + static propTypes = { + topic: PropTypes.object, + map: PropTypes.object, + filterData: PropTypes.object, + allForFiltering: PropTypes.object, + visibleForFiltering: PropTypes.object, + toggleMetacode: PropTypes.func, + toggleMapper: PropTypes.func, + toggleSynapse: PropTypes.func, + filterAllMetacodes: PropTypes.func, + filterAllMappers: PropTypes.func, + filterAllSynapses: PropTypes.func, + closeBox: PropTypes.func + } + + handleClickOutside = () => { + this.props.closeBox() + } + + render () { + const { topic, map, filterData, allForFiltering, visibleForFiltering, toggleMetacode, + toggleMapper, toggleSynapse, filterAllMetacodes, + filterAllMappers, filterAllSynapses } = this.props + const style = { + maxHeight: document.body.clientHeight - 108 + 'px' + } + const mapperAllClass = "showAll showAllMappers" + + (allForFiltering.mappers.length === visibleForFiltering.mappers.length ? ' active' : '') + const mapperNoneClass = "hideAll hideAllMappers" + + (visibleForFiltering.mappers.length === 0 ? ' active' : '') + const metacodeAllClass = "showAll showAllMetacodes" + + (allForFiltering.metacodes.length === visibleForFiltering.metacodes.length ? ' active' : '') + const metacodeNoneClass = "hideAll hideAllMetacodes" + + (visibleForFiltering.metacodes.length === 0 ? ' active' : '') + const synapseAllClass = "showAll showAllSynapses" + + (allForFiltering.synapses.length === visibleForFiltering.synapses.length ? ' active' : '') + const synapseNoneClass = "hideAll hideAllSynapses" + + (visibleForFiltering.synapses.length === 0 ? ' active' : '') + return map || topic ? <div className="sidebarFilterBox upperRightBox" style={style}> + <div className="filterBox"> + <h2>FILTER BY</h2> + <div id="filter_by_mapper" className="filterBySection"> + {map && <h3>MAPPERS</h3>} + {topic && <h3>CREATORS</h3>} + <span className={mapperNoneClass} onClick={() => filterAllMappers()}>NONE</span> + <span className={mapperAllClass} onClick={() => filterAllMappers(true)}>ALL</span> + <div className="clearfloat"></div> + <ul> + {allForFiltering.mappers.map(mapperId => { + const data = filterData.mappers[mapperId] + const isVisible = visibleForFiltering.mappers.indexOf(mapperId) > -1 + return <Mapper visible={isVisible} id={mapperId} image={data.image} name={data.name} toggle={toggleMapper} /> + })} + </ul> + <div className="clearfloat"></div> + </div> + + <div id="filter_by_metacode" className="filterBySection"> + <h3>METACODES</h3> + <span className={metacodeNoneClass} onClick={() => filterAllMetacodes()}>NONE</span> + <span className={metacodeAllClass} onClick={() => filterAllMetacodes(true)}>ALL</span> + <div className="clearfloat"></div> + <ul> + {allForFiltering.metacodes.map(metacodeId => { + const data = filterData.metacodes[metacodeId] + const isVisible = visibleForFiltering.metacodes.indexOf(metacodeId) > -1 + return <Metacode visible={isVisible} id={metacodeId} icon={data.icon} name={data.name} toggle={toggleMetacode} /> + })} + </ul> + <div className="clearfloat"></div> + </div> + + <div id="filter_by_synapse" className="filterBySection"> + <h3>SYNAPSES</h3> + <span className={synapseNoneClass} onClick={() => filterAllSynapses()}>NONE</span> + <span className={synapseAllClass} onClick={() => filterAllSynapses(true)}>ALL</span> + <div className="clearfloat"></div> + <ul> + {allForFiltering.synapses.map(synapseDesc => { + const data = filterData.synapses[synapseDesc] + const isVisible = visibleForFiltering.synapses.indexOf(synapseDesc) > -1 + return <Synapse visible={isVisible} desc={synapseDesc} icon={data.icon} toggle={toggleSynapse} /> + })} + </ul> + <div className="clearfloat"></div> + </div> + </div> + </div> : null + } +} + +function Mapper({ visible, name, id, image, toggle }) { + return <li onClick={() => toggle(id)} key={id} className={visible ? '' : 'toggledOff'}> + <img src={image} alt={name} /> + <p>{name}</p> + </li> +} + +function Metacode({ visible, name, id, icon, toggle }) { + return <li onClick={() => toggle(id)} key={id} className={visible ? '' : 'toggledOff'}> + <img src={icon} alt={name} /> + <p>{name.toLowerCase()}</p> + </li> +} + +function Synapse({ visible, desc, icon, toggle }) { + return <li onClick={() => toggle(desc)} key={desc.replace(/ /g, '')} className={visible ? '' : 'toggledOff'}> + <img src={icon} alt="synapse icon" /> + <p>{desc}</p> + </li> +} + +export default onClickOutsideAddon(FilterBox) diff --git a/frontend/src/components/common/InfoAndHelp.js b/frontend/src/components/common/InfoAndHelp.js new file mode 100644 index 00000000..fe298691 --- /dev/null +++ b/frontend/src/components/common/InfoAndHelp.js @@ -0,0 +1,38 @@ +import React, { Component, PropTypes } from 'react' + +import MapInfoBox from '../MapView/MapInfoBox' + +class InfoAndHelp extends Component { + static propTypes = { + mapIsStarred: PropTypes.bool, + currentUser: PropTypes.object, + map: PropTypes.object, + onHelpClick: PropTypes.func, + onMapStar: PropTypes.func, + onMapUnstar: PropTypes.func, + onInfoClick: PropTypes.func, + infoBoxhtml: PropTypes.string + } + + render () { + const { mapIsStarred, map, currentUser, onInfoClick, infoBoxHtml, onMapStar, onMapUnstar, onHelpClick } = this.props + const starclassName = mapIsStarred ? 'starred' : '' + const tooltip = mapIsStarred ? 'Unstar' : 'Star' + const onStarClick = mapIsStarred ? onMapUnstar : onMapStar + return <div className="infoAndHelp"> + {map && <MapInfoBox map={map} currentUser={currentUser} infoBoxHtml={infoBoxHtml} />} + {map && currentUser && <div className={`starMap infoElement mapElement ${starclassName}`} onClick={onStarClick}> + <div className="tooltipsAbove">{tooltip}</div> + </div>} + {map && <div className="mapInfoIcon infoElement mapElement" onClick={onInfoClick}> + <div className="tooltipsAbove">Map Info</div> + </div>} + <div className="openCheatsheet infoElement mapElement" onClick={onHelpClick}> + <div className="tooltipsAbove">Help</div> + </div> + <div className="clearfloat"></div> + </div> + } +} + +export default InfoAndHelp diff --git a/frontend/src/components/common/UpperOptions.js b/frontend/src/components/common/UpperOptions.js new file mode 100644 index 00000000..d17214ee --- /dev/null +++ b/frontend/src/components/common/UpperOptions.js @@ -0,0 +1,73 @@ +import React, { Component, PropTypes } from 'react' + +import FilterBox from '../common/FilterBox' + +export default class UpperOptions extends Component { + static propTypes = { + currentUser: PropTypes.object, + map: PropTypes.object, + topic: PropTypes.object, + canEditMap: PropTypes.bool, + onImportClick: PropTypes.func, + onForkClick: PropTypes.func, + filterData: PropTypes.object, + allForFiltering: PropTypes.object, + visibleForFiltering: PropTypes.object, + toggleMetacode: PropTypes.func, + toggleMapper: PropTypes.func, + toggleSynapse: PropTypes.func, + filterAllMetacodes: PropTypes.func, + filterAllMappers: PropTypes.func, + filterAllSynapses: PropTypes.func, + } + + constructor(props) { + super(props) + this.state = {filterBoxOpen: false} + } + + reset = () => { + this.setState({filterBoxOpen: false}) + } + + toggleFilterBox = event => { + this.setState({filterBoxOpen: !this.state.filterBoxOpen}) + } + + render () { + const { currentUser, map, topic, canEditMap, filterBoxHtml, onFilterClick, onImportClick, onForkClick, + filterData, allForFiltering, visibleForFiltering, toggleMetacode, toggleMapper, toggleSynapse, + filterAllMetacodes, filterAllMappers, filterAllSynapses } = this.props + const { filterBoxOpen } = this.state + return <div className="mapElement upperRightEl upperRightMapButtons upperRightUI"> + {map && canEditMap && <div className="importDialog upperRightEl upperRightIcon mapElement" onClick={onImportClick}> + <div className="tooltipsUnder"> + Import Data + </div> + </div>} + <div className="sidebarFilter upperRightEl"> + <div className="sidebarFilterIcon upperRightIcon ignore-react-onclickoutside" onClick={this.toggleFilterBox}> + <div className="tooltipsUnder">Filter</div> + </div> + {filterBoxOpen && <FilterBox filterData={filterData} + map={map} + topic={topic} + allForFiltering={allForFiltering} + visibleForFiltering={visibleForFiltering} + toggleMetacode={toggleMetacode} + toggleMapper={toggleMapper} + toggleSynapse={toggleSynapse} + filterAllMetacodes={filterAllMetacodes} + filterAllMappers={filterAllMappers} + filterAllSynapses={filterAllSynapses} + closeBox={() => this.reset()} />} + </div> + {currentUser && <div className="sidebarFork upperRightEl"> + <div className="sidebarForkIcon upperRightIcon" onClick={onForkClick}> + <div className="tooltipsUnder">Save To New Map</div> + </div> + </div>} + <div className="clearfloat"></div> + </div> + } +} diff --git a/frontend/src/components/common/VisualizationControls.js b/frontend/src/components/common/VisualizationControls.js new file mode 100644 index 00000000..0b45f827 --- /dev/null +++ b/frontend/src/components/common/VisualizationControls.js @@ -0,0 +1,25 @@ +import React, { Component, PropTypes } from 'react' + +export default class VisualizationControls extends Component { + static propTypes = { + map: PropTypes.object, + onClickZoomExtents: PropTypes.func, + onClickZoomIn: PropTypes.func, + onClickZoomOut: PropTypes.func + } + + render () { + const { map, onClickZoomExtents, onClickZoomIn, onClickZoomOut } = this.props + return <div className="mapControls mapElement"> + {map && <div className="zoomExtents mapControl" onClick={onClickZoomExtents}> + <div className="tooltips">Center View</div> + </div>} + <div className="zoomIn mapControl" onClick={onClickZoomIn}> + <div className="tooltips">Zoom In</div> + </div> + <div className="zoomOut mapControl" onClick={onClickZoomOut}> + <div className="tooltips">Zoom Out</div> + </div> + </div> + } +} diff --git a/frontend/src/components/makeRoutes.js b/frontend/src/components/makeRoutes.js new file mode 100644 index 00000000..45f8b746 --- /dev/null +++ b/frontend/src/components/makeRoutes.js @@ -0,0 +1,70 @@ +import React from 'react' +import { Route, IndexRoute } from 'react-router' +import App from './App' +import Maps from './Maps' +import MapView from './MapView' +import TopicView from './TopicView' + +function nullComponent(props) { + return null +} + +export default function makeRoutes (currentUser) { + const homeComponent = currentUser && currentUser.id ? Maps : nullComponent + return <Route path="/" component={App} > + <IndexRoute component={homeComponent} /> + <Route path="explore"> + <Route path="active" component={Maps} /> + <Route path="featured" component={Maps} /> + <Route path="mine" component={Maps} /> + <Route path="shared" component={Maps} /> + <Route path="starred" component={Maps} /> + <Route path="mapper/:id" component={Maps} /> + </Route> + <Route path="maps/:id"> + <IndexRoute component={MapView} /> + <Route path="conversation" component={MapView} /> + <Route path="request_access" component={nullComponent} /> + </Route> + <Route path="topics/:id" component={TopicView} /> + <Route path="login" component={nullComponent} /> + <Route path="join" component={nullComponent} /> + <Route path="request" component={nullComponent} /> + <Route path="notifications"> + <IndexRoute component={nullComponent} /> + <Route path=":id" component={nullComponent} /> + </Route> + <Route path="users"> + <Route path=":id/edit" component={nullComponent} /> + <Route path="password/new" component={nullComponent} /> + <Route path="password/edit" component={nullComponent} /> + </Route> + <Route path="metacodes"> + <IndexRoute component={nullComponent} /> + <Route path="new" component={nullComponent} /> + <Route path=":id/edit" component={nullComponent} /> + </Route> + <Route path="metacode_sets"> + <IndexRoute component={nullComponent} /> + <Route path="new" component={nullComponent} /> + <Route path=":id/edit" component={nullComponent} /> + </Route> + <Route path="oauth"> + <Route path="token/info" component={nullComponent} /> + <Route path="authorize"> + <IndexRoute component={nullComponent} /> + <Route path=":code" component={nullComponent} /> + </Route> + <Route path="authorized_applications"> + <IndexRoute component={nullComponent} /> + <Route path=":id" component={nullComponent} /> + </Route> + <Route path="applications"> + <IndexRoute component={nullComponent} /> + <Route path="new" component={nullComponent} /> + <Route path=":id" component={nullComponent} /> + <Route path=":id/edit" component={nullComponent} /> + </Route> + </Route> + </Route> +} diff --git a/package.json b/package.json index 84eff416..6c259080 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-dom": "15.4.2", "react-dropzone": "3.9.1", "react-onclickoutside": "5.9.0", + "react-router": "3.0.2", "redux": "3.6.0", "riek": "1.0.7", "simplewebrtc": "2.2.2",