diff --git a/app/assets/javascripts/createSession.js b/app/assets/javascripts/createSession.js index 66c2652d6..5fc5d3e2f 100644 --- a/app/assets/javascripts/createSession.js +++ b/app/assets/javascripts/createSession.js @@ -126,9 +126,7 @@ Message from Seth on sequence for creating/joining sessions: self.location = '#/session/' + newSessionId; }); } - ).fail(function(jqXHR, textStatus, errorMessage) { - app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); - }); + ).fail(app.ajaxError); return false; } @@ -147,9 +145,7 @@ Message from Seth on sequence for creating/joining sessions: data: invite }).done(function(response) { callCount--; - }).fail(function(jqXHR, textStatus, errorMessage) { - app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); - }); + }).fail(app.ajaxError); }); // TODO - this is the second time I've used this pattern. // refactor to make a common utility for this. diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 6c0733b90..82ac19c0b 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -9,6 +9,7 @@ context.JK.Header = function(app) { var logger = context.JK.logger; + var searcher; // Will hold an instance to a JK.Searcher (search.js) var userMe = null; var instrumentAutoComplete; var instrumentIds = []; @@ -97,9 +98,7 @@ data: JSON.stringify(user) }).done(function(response) { userMe = response; - }).fail(function(jqXHR, textStatus, errorMessage) { - app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); - }); + }).fail(app.ajaxError); return false; } @@ -134,9 +133,7 @@ data: JSON.stringify(user) }).done(function(response) { userMe = response; - }).fail(function(jqXHR, textStatus, errorMessage) { - app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); - }); + }).fail(app.ajaxError); return false; } @@ -149,9 +146,7 @@ }).done(function(r) { userMe = r; updateAccountForms(); - }).fail(function(jqXHR, textStatus, errorMessage) { - app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); - }); + }).fail(app.ajaxError); } function updateAccountForms() { @@ -178,6 +173,9 @@ events(); loadInstruments(); loadMe(); + + searcher = new JK.Searcher(app); + searcher.initialize(); }; }; diff --git a/app/assets/javascripts/jamkazam.js b/app/assets/javascripts/jamkazam.js index d01b895bd..761854030 100644 --- a/app/assets/javascripts/jamkazam.js +++ b/app/assets/javascripts/jamkazam.js @@ -76,6 +76,13 @@ context.JK.MessageType.LOGIN_ACK, loggedIn); } + /** + * Generic error handler for Ajax calls. + */ + function ajaxError(jqXHR, textStatus, errorMessage) { + notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); + } + /** * Register for all known types, logging events as they happen, and * notifying subscribers (see this.subscribe) as they occur. @@ -107,6 +114,11 @@ */ this.fireEvent = handleMessage; + /** + * Expose ajaxError. + */ + this.ajaxError = ajaxError; + /** * Provide a handler object for events related to a particular screen * being shown or hidden. diff --git a/app/assets/javascripts/joinSession.js b/app/assets/javascripts/joinSession.js index dd7a404e7..bca699f46 100644 --- a/app/assets/javascripts/joinSession.js +++ b/app/assets/javascripts/joinSession.js @@ -27,7 +27,7 @@ client.JoinSession({ sessionID: session_id }); } context.JK.refreshMusicSession(session_id); - }); + }).fail(app.ajaxError); }; })(window,jQuery); \ No newline at end of file diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js new file mode 100644 index 000000000..200fa0c53 --- /dev/null +++ b/app/assets/javascripts/search.js @@ -0,0 +1,129 @@ +(function(context,$) { + + context.JK = context.JK || {}; + context.JK.Searcher = function(app) { + var logger = context.JK.logger; + + var searchSectionTemplate; + var searchItemTemplate; + var noResultsTemplate; + + function events() { + $('.searchtextinput').keyup(handleKeyup); + $('.searchtextinput').focus(function(evt) { + var searchVal = $(this).val(); + search(searchVal); + }); + $('.searchtextinput').blur(hideSearchResults); + } + + function templates() { + searchSectionTemplate = $('#template-search-section').html(); + searchItemTemplate = $('#template-search-item').html(); + noResultsTemplate = $('#template-search-noresults').html(); + } + + function hideSearchResults() { + $('.searchresults').hide(); + } + + function showSearchResults() { + $('.searchresults').show(); + } + + function handleKeyup(evt) { + if (evt.which === 27) { + return hideSearchResults(); + } + var searchVal = $(this).val(); + search(searchVal); + } + + function search(query) { + if (query.length < 2) { + return; + } + $.ajax({ + type: "GET", + url: "/api/search?query=" + query, + success: searchResponse, + error: app.ajaxError + }); + } + + function searchResponse(response) { + ensureResultsDiv(); + updateResultsDiv(response); + positionResultsDiv(); + showSearchResults(); + } + + function ensureResultsDiv() { + if ($('.searchresults').length === 0) { + $searchresults = $('
'); + $searchresults.addClass('searchresults'); + $searchresults.css({position:'absolute'}); + $('body').append($searchresults); + } + } + + function updateResultsDiv(searchResults) { + var sections = ['musicians', 'bands', 'fans', 'recordings']; + var fullHtml = ''; + $.each(sections, function() { + fullHtml += getSectionHtml(this, searchResults); + }); + if (fullHtml === '') { + fullHtml += getNoResultsMessage(); + } + $('.searchresults').html(fullHtml); + } + + function getNoResultsMessage() { + // No replacement needed at the moment. + return noResultsTemplate; + } + + function getSectionHtml(section, searchResults) { + if (section in searchResults && searchResults[section].length === 0) { + return ''; + } + var items = ''; + $.each(searchResults[section], function() { + items += getItemHtml(this); + }); + var html = JK.fillTemplate( + searchSectionTemplate, + { section: section, items: items }); + return html; + } + + function getItemHtml(item) { + var replacements = { + id: item.id, + name: item.first_name + " " + item.last_name, + image: item.photo_url, + subtext: item.location + }; + return JK.fillTemplate( + searchItemTemplate, replacements); + } + + function positionResultsDiv() { + var bodyOffset = $('body').offset(); + var inputOffset = $('.searchtextinput').offset(); + var inputHeight = $('.searchtextinput').outerHeight(); + var resultsTop = bodyOffset.top + inputOffset.top + inputHeight; + var resultsLeft = bodyOffset.left + inputOffset.left; + $('.searchresults').css({ + top: resultsTop + 'px', + left: resultsLeft + 'px'}); + } + + this.initialize = function() { + events(); + templates(); + }; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/app/assets/stylesheets/client/search.css.scss b/app/assets/stylesheets/client/search.css.scss new file mode 100644 index 000000000..488e053c0 --- /dev/null +++ b/app/assets/stylesheets/client/search.css.scss @@ -0,0 +1,57 @@ +/* Styles used by things related to search */ +@import "client/common.css.scss"; + +/* Container for the search input */ +.header .search { + position: absolute; + left: 50%; + margin-left: -125px; + top: 26px; +} + +.searchtextinput { + border: 1px solid #fff; + background:none; + color:#fff; + font-size: 120%; + line-height: 120%; + width: 250px; + padding: 6px; +} + +.searchresults { + background-color:$color8; + border:1px solid #000; + color:#000; +} + +.searchresults h2 { + font-size: 120%; + font-weight: bold; + background-color: shade($color8, 10%); +} + +.searchresults li { + clear:both; + padding: 8px; +} + +.searchresults img { + float:left; + width: 32px; + height: 32px; + border: 1px solid #333; + margin: 4px; +} + +.searchresults .text { + color: #000; + font-weight: bold; +} + +.searchresults .subtext { + display:block; + color: #000; + font-size: 90%; +} + diff --git a/app/views/api_search/index.rabl b/app/views/api_search/index.rabl index fe51b2ef9..984df0142 100644 --- a/app/views/api_search/index.rabl +++ b/app/views/api_search/index.rabl @@ -14,4 +14,4 @@ child(:fans => :fans) { child(:recordings => :recordings) { attributes :id, :name -} \ No newline at end of file +} diff --git a/app/views/clients/index.html.erb b/app/views/clients/index.html.erb index 59c0ebbed..debe69e50 100644 --- a/app/views/clients/index.html.erb +++ b/app/views/clients/index.html.erb @@ -1,6 +1,32 @@

JamKazam

+ + + + + + + +
<%= gravatar_for current_user, size: 52, hclass: "avatar medium" %>
diff --git a/app/views/layouts/client.html.erb b/app/views/layouts/client.html.erb index f566f6f12..2cebfbacb 100644 --- a/app/views/layouts/client.html.erb +++ b/app/views/layouts/client.html.erb @@ -7,6 +7,7 @@ <%= stylesheet_link_tag "client/ie", media: "all" %> <%= stylesheet_link_tag "client/jamkazam", media: "all" %> + <%= stylesheet_link_tag "client/search", media: "all" %> <%= stylesheet_link_tag "client/lato", media: "all" %> <%= include_gon %> <%= javascript_include_tag "application" %> diff --git a/spec/javascripts/fixtures/searcher.htm b/spec/javascripts/fixtures/searcher.htm new file mode 100644 index 000000000..9992a1f20 --- /dev/null +++ b/spec/javascripts/fixtures/searcher.htm @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/spec/javascripts/helpers/test_responses.js b/spec/javascripts/helpers/test_responses.js new file mode 100644 index 000000000..9327d6f2c --- /dev/null +++ b/spec/javascripts/helpers/test_responses.js @@ -0,0 +1,62 @@ +window.TestResponses = { + search : { + bands: [ ], + musicians: [ ], + fans: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + recordings: [ ] + }, + + emptySearch: { + bands: [], + musicians: [], + fans: [], + recordings: [] + }, + + fullSearch: { + bands: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + musicians: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + fans: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + recordings: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ] + } +}; \ No newline at end of file diff --git a/spec/javascripts/searcher.spec.js b/spec/javascripts/searcher.spec.js new file mode 100644 index 000000000..067a8b8df --- /dev/null +++ b/spec/javascripts/searcher.spec.js @@ -0,0 +1,196 @@ +(function(context) { + + describe("searcher.js tests", function() { + + describe("Empty Search", function() { + + // See the markup in fixtures/searcher.htm + + var searcher; + var ajaxSpy; + + var fakeApp = { + ajaxError: function() { + console.debug("ajaxError"); + } + }; + + beforeEach(function() { + loadFixtures('searcher.htm'); + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.emptySearch); + }); + searcher = new JK.Searcher(fakeApp); + searcher.initialize(); + }); + + it("No Results message shown", function() { + // Workaround for key events not being reflected in val() calls + $('.searchtextinput').val('AA'); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').focus(); + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults .emptyresult').length).toEqual(1); + }); + }); + + describe("Full Search", function() { + + // See the markup in fixtures/searcher.htm + + var searcher; + var ajaxSpy; + + var fakeApp = { + ajaxError: function() { + console.debug("ajaxError"); + } + }; + + beforeEach(function() { + loadFixtures('searcher.htm'); + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.fullSearch); + }); + searcher = new JK.Searcher(fakeApp); + searcher.initialize(); + }); + + it("No Results message shown", function() { + // Workaround for key events not being reflected in val() calls + $('.searchtextinput').val('AA'); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').focus(); + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults h2').length).toEqual(4); + }); + }); + + describe("Search Tests", function() { + + // See the markup in fixtures/searcher.htm + + var searcher; + var ajaxSpy; + + var fakeApp = { + ajaxError: function() { + console.debug("ajaxError"); + } + }; + + beforeEach(function() { + loadFixtures('searcher.htm'); + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.search); + }); + searcher = new JK.Searcher(fakeApp); + searcher.initialize(); + }); + + it("first keypress should not search", function() { + // Workaround for key events not being reflected in val() calls + $('.searchtextinput').val('A'); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').focus(); + $('.searchtextinput').trigger(e); + expect($.ajax.wasCalled).toBe(false); + }); + + it("second keypress should search", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + // trigger again + $('.searchtextinput').trigger(e); + + expect($.ajax).toHaveBeenCalled(); + }); + + it("response div is absolute position", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + expect($('.searchresults').css('position')).toEqual('absolute'); + }); + + it("response displayed in results", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults').length).toEqual(1); + expect($('.searchresults h2').length).toEqual(1); + expect($('.searchresults li').length).toEqual(1); + expect($('.searchresults li img').length).toEqual(1); + expect($('.searchresults li span.text').length).toEqual(1); + expect($('.searchresults li span.subtext').length).toEqual(1); + }); + + + it("response positioned under input", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults').length).toEqual(1); + var bodyOffset = $('body').offset(); + var inputOffset = $('.searchtextinput').offset(); + var inputHeight = $('.searchtextinput').outerHeight(); + var expectedTop = bodyOffset.top + inputOffset.top + inputHeight; + var expectedLeft = bodyOffset.left + inputOffset.left; + + var searchResultOffset = $('.searchresults').offset(); + expect(searchResultOffset.top).toEqual(expectedTop); + expect(searchResultOffset.left).toEqual(expectedLeft); + }); + + it("search results are visible", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + var visible = $('.searchresults').is(':visible'); + expect(visible).toBe(true); + }); + + it("escape key hides search results", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + e = jQuery.Event("keyup"); + e.which = 27; // ESCAPE + $('.searchtextinput').trigger(e); + + var visible = $('.searchresults').is(':visible'); + expect(visible).toBe(false); + }); + }); + + }); +})(window); \ No newline at end of file