* VRFS-72 complete

This commit is contained in:
Seth Call 2012-11-12 07:20:38 -06:00
commit 00bd6e4569
17 changed files with 500 additions and 38 deletions

View File

@ -0,0 +1,184 @@
(function(context,$) {
/**
* Javascript for managing the header (account dropdown) as well
* as any dialogs reachable from there. Account settings dialog.
*/
context.JK = context.JK || {};
context.JK.Header = function(app) {
var logger = context.JK.logger;
var userMe = null;
var instrumentAutoComplete;
var instrumentIds = [];
var instrumentNames = [];
var instrumentPopularities = {}; // id -> popularity
function loadInstruments() {
// TODO: This won't work in the long-term. We'll need to provide
// a handlers which accepts some characters and only returns users
// who are musicians who match that input string. Once we get there,
// we could just use the ajax functionality of the autocomplete plugin.
//
// But for now:
// Load the users list into our local array for autocomplete.
$.ajax({
type: "GET",
url: "/api/instruments"
}).done(function(response) {
$.each(response, function() {
instrumentNames.push(this.description);
instrumentIds.push(this.id);
// TODO - unused at the moment.
instrumentPopularities[this.id] = this.popularity;
});
// Hook up the autocomplete.
var options = {
lookup: {suggestions:instrumentNames, data: instrumentIds},
onSelect: addInstrument,
width: 120
};
if (!(instrumentAutoComplete)) { // Shouldn't happen here. Header only drawn once.
instrumentAutoComplete = $('#profile-instruments').autocomplete(options);
} else {
instrumentAutoComplete.setOptions(options);
}
});
}
function addInstrument(value, data) {
var instrumentName = value;
var instrumentId = data;
var template = $('#template-profile-instrument').html(); // TODO: cache this
var instrumentHtml = JK.fillTemplate(
template, {instrumentId: instrumentId, instrumentName: instrumentName});
$('#added-profile-instruments').append(instrumentHtml);
$('#profile-instruments').select();
}
function setProficiency(id, proficiency) {
logger.debug("setProficiency: " + id + ',' + proficiency);
var selector = '#added-profile-instruments div.profile-instrument[instrument-id="' +
id + '"] select';
logger.debug("Finding select to set proficiency. Length? " + $(selector).length);
$(selector).val(proficiency);
}
function removeInvitation(evt) {
$(this).closest('.profile-instrument').remove();
}
function events() {
$('.username').on('click', function() {
$('ul', this).toggle();
});
$('#account-identity-form').submit(handleIdentitySubmit);
$('#account-profile-form').submit(handleProfileSubmit);
// Remove added instruments when 'X' is clicked
$('#added-profile-instruments').on("click", ".instrument span", removeInvitation);
}
function handleIdentitySubmit(evt) {
evt.preventDefault();
user = $(this).formToObject();
if (!user.password) {
delete user.password;
delete user.password_confirmation;
}
logger.debug("submitting identity form with:");
logger.debug(user);
$.ajax({
type: "POST",
url: "/api/users/" + userMe.id,
contentType: "application/json",
processData:false,
data: JSON.stringify(user)
}).done(function(response) {
userMe = response;
}).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
return false;
}
function handleProfileSubmit(evt) {
evt.preventDefault();
var user = {
name: $('#account-profile-form input[name="name"]').val(),
instruments: []
};
$added_instruments = $('.profile-instrument', '#added-profile-instruments');
var count = 1;
$.each($added_instruments, function() {
var instrumentId = $(this).attr('instrument-id');
var proficiency = $('select', this).val();
logger.debug("Instrument ID:" + instrumentId + ", proficiency: " + proficiency);
var instrument = {
id: instrumentId,
proficiency_level: proficiency,
priority: count++
};
user.instruments.push(instrument);
});
logger.debug("About to submit profile. User:");
logger.debug(user);
$.ajax({
type: "POST",
url: "/api/users/" + userMe.id,
contentType: "application/json",
processData:false,
data: JSON.stringify(user)
}).done(function(response) {
userMe = response;
}).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
return false;
}
function loadMe() {
logger.debug("loadMe");
logger.debug("current user: " + JK.currentUserId);
$.ajax({
url: '/api/users/' + JK.currentUserId
}).done(function(r) {
userMe = r;
updateAccountForms();
}).fail(function(jqXHR, textStatus, errorMessage) {
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
});
}
function updateAccountForms() {
var idTemplate = $('#template-identity-summary').html();
var idHtml = JK.fillTemplate(idTemplate, userMe);
$('#identity-summary').html(idHtml);
// TODO:
// Make a thing that puts a JSON object into a form
// and fill the profile part of the form from the JSON.
// Short-term thing for now:
$('#account-identity-form input[name="email"]').val(userMe.email);
// Profile form
$('#account-profile-form input[name="name"]').val(userMe.name);
if ("instruments" in userMe) {
$.each(userMe.instruments, function() {
addInstrument(this.description, this.id);
setProficiency(this.id, this.proficiency_level);
});
}
}
this.initialize = function() {
events();
loadInstruments();
loadMe();
};
};
})(window,jQuery);

View File

@ -0,0 +1,20 @@
(function(context,$) {
context.JK = context.JK || {};
context.JK.HomeScreen = function(app) {
function events() {
$('.homecard').on('mouseenter', function() {
$(this).addClass('hover');
});
$('.homecard').on('mouseleave', function() {
$(this).removeClass('hover');
});
}
this.initialize = function() {
events();
};
};
})(window,jQuery);

View File

@ -37,18 +37,6 @@
$(routes.handler);
}
/**
* Some simple events for hovering on the home page. May want to move to a separate file.
*/
function events() {
$('.homecard').on('mouseenter', function() {
$(this).addClass('hover');
});
$('.homecard').on('mouseleave', function() {
$(this).removeClass('hover');
});
}
/**
* Handle a websocket message
*/
@ -72,13 +60,13 @@
}
function _heartbeat() {
//logger.debug("...sending heartbeat");
message = context.JK.MessageFactory.heartbeat();
context.JK.JamServer.send(message);
}
function loggedIn(header, payload) {
logger.debug('loggedIn');
logger.debug(payload);
app.clientId = payload.client_id;
}
@ -97,7 +85,7 @@
logger.debug("registering " + message);
context.JK.JamServer.registerMessageCallback(message, handleMessage);
}
// Specifically register a handler for LOGIN_ACK to setup the heartbeat calls.
// Setup the heartbeat calls:
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, _handleLoginAck);
}
@ -151,7 +139,6 @@
routing();
registerMessages();
registerLoginAck();
events();
hash = location.hash;
url = '#/home';

View File

@ -0,0 +1,106 @@
/**
* hoverIntent is similar to jQuery's built-in "hover" function except that
* instead of firing the onMouseOver event immediately, hoverIntent checks
* to see if the user's mouse has slowed down (beneath the sensitivity
* threshold) before firing the onMouseOver event.
*
* hoverIntent r6 // 2011.02.26 // jQuery 1.5.1+
* <http://cherne.net/brian/resources/jquery.hoverIntent.html>
*
* hoverIntent is currently available for use in all personal or commercial
* projects under both MIT and GPL licenses. This means that you can choose
* the license that best suits your project, and use it accordingly.
*
* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions
* $("ul li").hoverIntent( showNav , hideNav );
*
* // advanced usage receives configuration object only
* $("ul li").hoverIntent({
* sensitivity: 7, // number = sensitivity threshold (must be 1 or higher)
* interval: 100, // number = milliseconds of polling interval
* over: showNav, // function = onMouseOver callback (required)
* timeout: 0, // number = milliseconds delay before onMouseOut function call
* out: hideNav // function = onMouseOut callback (required)
* });
*
* @param f onMouseOver function || An object with configuration options
* @param g onMouseOut function || Nothing (use configuration options object)
* @author Brian Cherne brian(at)cherne(dot)net
*/
(function($) {
$.fn.hoverIntent = function(f,g) {
// default configuration options
var cfg = {
sensitivity: 7,
interval: 100,
timeout: 0
};
// override configuration options with user supplied object
cfg = $.extend(cfg, g ? { over: f, out: g } : f );
// instantiate variables
// cX, cY = current X and Y position of mouse, updated by mousemove event
// pX, pY = previous X and Y position of mouse, set by mouseover and polling interval
var cX, cY, pX, pY;
// A private function for getting mouse position
var track = function(ev) {
cX = ev.pageX;
cY = ev.pageY;
};
// A private function for comparing current and previous mouse position
var compare = function(ev,ob) {
ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
// compare mouse positions to see if they've crossed the threshold
if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) {
$(ob).unbind("mousemove",track);
// set hoverIntent state to true (so mouseOut can be called)
ob.hoverIntent_s = 1;
return cfg.over.apply(ob,[ev]);
} else {
// set previous coordinates for next time
pX = cX; pY = cY;
// use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs)
ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval );
}
};
// A private function for delaying the mouseOut function
var delay = function(ev,ob) {
ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t);
ob.hoverIntent_s = 0;
return cfg.out.apply(ob,[ev]);
};
// A private function for handling mouse 'hovering'
var handleHover = function(e) {
// copy objects to be passed into t (required for event object to be passed in IE)
var ev = jQuery.extend({},e);
var ob = this;
// cancel hoverIntent timer if it exists
if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); }
// if e.type == "mouseenter"
if (e.type == "mouseenter") {
// set "previous" X and Y position based on initial entry point
pX = ev.pageX; pY = ev.pageY;
// update "current" X and Y position based on mousemove
$(ob).bind("mousemove",track);
// start polling interval (self-calling timeout) to compare mouse coordinates over time
if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );}
// else e.type == "mouseleave"
} else {
// unbind expensive mousemove event
$(ob).unbind("mousemove",track);
// if hoverIntent state is true, then call the mouseOut function after the specified delay
if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );}
}
};
// bind the function to the two event listeners
return this.bind('mouseenter',handleHover).bind('mouseleave',handleHover);
};
})(jQuery);

View File

@ -64,6 +64,15 @@ label {
display:block;
}
.button1 {
background: $color1;
color: #fff !important;
font-weight:bold;
font-size: 120%;
margin: 2px;
padding:8px;
}
.hidden {
display:none;
}
@ -82,6 +91,31 @@ label {
font-weight:bold;
}
.dialog {
background-color:$color8;
border: #666;
color:#000;
min-width: 400px;
min-height: 350px;
}
.dialog a {
color: $color1;
}
.dialog h1 {
font-weight: bold;
font-size: 150%;
margin: 12px;
}
.dialog .panel {
background-color: shade($color8, 6%);
margin:12px;
padding: 12px;
}
.header h1 {
margin:22px;
font-size:300%;
@ -126,7 +160,7 @@ label {
.screen.secondary {
}
.screen.secondary .footer {
.buttonrow, .screen.secondary .footer {
position: absolute;
bottom:0px;
right:0px;
@ -279,7 +313,8 @@ label {
clear:both;
}
.invitation {
/* TODO - generalize */
.instrument, .invitation {
margin: 1px;
background: $color8;
color:#000;
@ -288,12 +323,16 @@ label {
float:left;
}
.invitation span {
.instrument span, .invitation span {
font-size:85%;
font-weight:bold;
cursor:pointer;
}
.profiency {
float:left;
}
/* Autocomplete */
.autocomplete {
border:1px solid #999;

View File

@ -145,7 +145,7 @@ class ApiMusicSessionsController < ApiController
# send out notification to queue to the rest of the session
# TODO: we should rename the notification to music_session_participants_change or something
# TODO: also this isn't necessarily a user leaving; it's a client leaving'
user_left = @message_factory.user_left_music_session(@music_session.id, current_user.id, current_user.name)
user_left = @message_factory.user_left_music_session(@music_session.id, current_user.id, current_user.first_name + " " + current_user.last_name)
@mq_router.server_publish_to_session(@music_session, user_left, sender = {:client_id => @connection.client_id})
end

View File

@ -112,7 +112,7 @@ class ApiUsersController < ApiController
def following_destroy
JamRuby::UserFollower.delete_all "(user_id = '#{params[:user_id]}' AND follower_id = '#{params[:id]}')"
#JamRuby::BandFollower.delete_all "(ban_id = '#{params[:band_id]}' AND follower_id = '#{params[:id]}')"
#JamRuby::BandFollower.delete_all "(band_id = '#{params[:band_id]}' AND follower_id = '#{params[:id]}')"
respond_with responder: ApiResponder
end

View File

@ -2,6 +2,14 @@ object @band.followers
attributes :follower_id => :user_id
node :first_name do |follower|
follower.user.first_name
end
node :last_name do |follower|
follower.user.last_name
end
node :name do |follower|
follower.user.name
end

View File

@ -4,7 +4,7 @@ attributes :id, :name, :city, :state, :country, :website, :biography, :photo_url
unless @band.users.nil? || @band.users.size == 0
child :users => :musicians do
attributes :id, :name, :photo_url
attributes :id, :first_name, :last_name, :name, :photo_url
# TODO: figure out how to omit empty arrays
node :instruments do |user|

View File

@ -5,11 +5,11 @@ child(:bands => :bands) {
}
child(:musicians => :musicians) {
attributes :id, :name, :location, :photo_url
attributes :id, :first_name, :last_name :location, :photo_url
}
child(:fans => :fans) {
attributes :id, :name, :location, :photo_url
attributes :id, :first_name, :last_name, :location, :photo_url
}
child(:recordings => :recordings) {

View File

@ -2,6 +2,14 @@ object @user.followers
attributes :follower_id => :user_id
node :first_name do |follower|
follower.user.first_name
end
node :last_name do |follower|
follower.user.last_name
end
node :name do |follower|
follower.user.name
end

View File

@ -2,8 +2,16 @@ object @user.followings
attributes :user_id
node :name do |following|
following.user.name
node :first_name do |following|
following.user.first_name
end
node :last_name do |following|
following.user.last_name
end
node :name do |follower|
follower.user.name
end
node :city do |following|

View File

@ -1,3 +1,3 @@
object @user.friends
attributes :id, :name, :city, :state, :country, :email, :online
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online

View File

@ -1,4 +1,4 @@
collection @users
# do not retrieve all child collections when showing a list of users
attributes :id, :name, :city, :state, :country, :email, :online, :musician, :photo_url
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url

View File

@ -1,10 +1,10 @@
object @user
attributes :id, :name, :city, :state, :country, :online, :photo_url, :friend_count, :follower_count, :following_count
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :photo_url, :friend_count, :follower_count, :following_count
unless @user.friends.nil? || @user.friends.size == 0
child :friends => :friends do
attributes :id, :name, :online
attributes :id, :first_name, :last_name, :name, :online
end
end

View File

@ -7,7 +7,7 @@
<h2><%= current_user.name %></h2>
<%= image_tag "down_arrow.png", :class=> "profile-toggle" %>
<ul>
<li>Profile</li>
<li><a layout-link="account">Profile</a></li>
<li><%= link_to "Sign out", signout_path, method: "delete" %></li>
</ul>
</div>
@ -39,6 +39,7 @@
</div>
</div>
</div>
<div layout="sidebar" class="sidebar">
<div layout-sidebar-expander="visible" class="expander visible">
<p>&gt;&gt; Hide</p>
@ -427,10 +428,104 @@
<p layout-link="home">Home</p>
</div>
<div layout="dialog" layout-id="dialog1" class="dialog">
<h1>Dialog 1</h1>
<a layout-action="close">Close</a>
<!-- Account Summary Dialog -->
<div layout="dialog" layout-id="account" class="dialog">
<h1>Account Settings</h1>
<div class="panel">
<h2>Identity</h2>
<div id="identity-summary"></div>
<a layout-link="account-identity">Update</a>
</div>
<div class="panel">
<h2>Profile</h2>
<div id="profile-summary"></div>
<a layout-link="account-profile">Update</a>
</div>
<div class="buttonrow">
<a layout-action="close" class="button1">Close</a>
</div>
</div>
<script type="text/template" id="template-identity-summary">
<p>Username: {email}</p>
<p>Change Password Here</p>
</script>
<!-- Account Identity Update Form -->
<div layout="dialog" layout-id="account-identity" class="dialog">
<form id="account-identity-form">
<fieldset>
<legend>Identity</legend>
<div class="formrow">
<label>Email
<input name="email" type="text"/>
</label>
</div>
<div class="formrow">
<label>Password
<input name="password" type="password"/>
</label>
<p class="tip">Leave blank to leave unchanged</p>
</div>
<div class="formrow">
<label>Confirm Password
<input name="password_confirmation" type="password"/>
</label>
</div>
<div class="buttonrow">
<button layout-action="close">Cancel</button>
<input type="submit" value"Update"/>
</div>
</fieldset>
</form>
</div>
<!-- Account Profile Update Form -->
<div layout="dialog" layout-id="account-profile" class="dialog">
<form id="account-profile-form">
<fieldset>
<legend>Profile</legend>
<div class="formrow">
<label>Name
<input type="text" name="name"/>
</label>
</div>
<div class="formrow">
<label>Profile Picture
<p>TO DO</p>
</label>
</div>
<div class="formrow">
<div id="added-profile-instruments"></div>
<div id="profile-instruments-controls">
<label>Instruments
<input id="profile-instruments" type="text"/>
</label>
<p class="tip">Limit of 5</p>
</div>
</div>
<div class="buttonrow">
<button layout-action="close">Cancel</button>
<input type="submit" value"Update"/>
</div>
</fieldset>
</form>
</div>
<script type="text/template" id="template-profile-instrument">
<div class="profile-instrument" instrument-id="{instrumentId}">
<div class="instrument">{instrumentName} <span>X</span></div>
<div class="proficiency">
<select>
<option value="1">Beginner</option>
<option selected="selected" value="2">Intermediate</option>
<option value="3">Advanced</option>
<option value="4">Expert</option>
</select>
</div>
</div>
</script>
<div layout="notify" class="notify">
<h1>Notification Popup</h1>
<a layout-action="close">Close</a>
@ -440,9 +535,19 @@
<script type="text/javascript">
$(function() {
var jk = JK.JamKazam();
jk.initialize();
JK.currentUserId = '<%= current_user.id %>';
var header = new JK.Header(jk);
header.initialize();
var homeScreen = new JK.HomeScreen(jk);
homeScreen.initialize();
var createSessionScreen = new JK.CreateSessionScreen(jk);
createSessionScreen.initialize();
@ -454,6 +559,7 @@
var jam_server = JK.JamServer;
jam_server.connect();
})
</script>

View File

@ -78,11 +78,7 @@ SampleApp::Application.routes.draw do
# band followers
match '/bands/:id/followers' => 'api_bands#follower_index', :via => :get
=begin
# band followings
match '/bands/:id/followings' => 'api_bands#following_create', :via => :post
match '/bands/:id/followings/:user_id' => 'api_bands#following_destroy', :via => :delete
=end
# invitations
match '/invitations/:id' => 'api_invitations#show', :via => :get, :as => 'api_invitation_detail'
match '/invitations/:id' => 'api_invitations#delete', :via => :delete