Skip to content

Commit

Permalink
parties: validation + code cleanups
Browse files Browse the repository at this point in the history
  • Loading branch information
gschmidt committed Oct 16, 2012
1 parent 0ca1386 commit 4dee258
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 229 deletions.
175 changes: 86 additions & 89 deletions examples/parties/client/client.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
// XXX I want to collect first/last name when a user signs up by username/password
// All Tomorrow's Parties -- client

Meteor.subscribe("directory");
Meteor.subscribe("parties");

Template.page.showCreateDialog = function () {
return Session.get("showCreateDialog");
};

Template.page.showInviteDialog = function () {
return Session.get("showInviteDialog");
};

// If no party selected, select one.
Meteor.startup(function () {
Meteor.autorun(function () {
Expand All @@ -24,7 +16,6 @@ Meteor.startup(function () {

///////////////////////////////////////////////////////////////////////////////
// Party details sidebar
///////////////////////////////////////////////////////////////////////////////

Template.details.party = function () {
return Parties.findOne(Session.get("selected"));
Expand All @@ -41,34 +32,6 @@ Template.details.creatorName = function () {
return displayName(owner);
};

Template.attendance.rsvpName = function () {
var user = Meteor.users.findOne(this.user);
return displayName(user);
};

Template.attendance.outstandingInvitations = function () {
var party = Parties.findOne(this._id);
// take out the people that have already rsvp'd
var people = _.difference(party.invited, _.pluck(party.rsvps, 'user'));
return Meteor.users.find({_id: {$in: people}});
};

Template.attendance.invitationName = function () {
return displayName(this);
};

Template.attendance.rsvpIs = function (what) {
return this.rsvp === what;
};

Template.attendance.nobody = function () {
return ! this.public && (this.rsvps.length + this.invited.length === 0);
};

Template.attendance.canInvite = function () {
return ! this.public && this.owner === Meteor.userId();
};

Template.details.canRemove = function () {
return this.owner === Meteor.userId() && attending(this) === 0;
};
Expand All @@ -81,9 +44,7 @@ Template.details.maybeChosen = function (what) {
return what == myRsvp.rsvp ? "chosen btn-inverse" : "";
};

// XXX show which button is currently selected
Template.details.events({
// XXX demonstrate error handling?
'click .rsvp_yes': function () {
Meteor.call("rsvp", Session.get("selected"), "yes");
return false;
Expand All @@ -97,7 +58,7 @@ Template.details.events({
return false;
},
'click .invite': function () {
Session.set("showInviteDialog", true);
openInviteDialog();
return false;
},
'click .remove': function () {
Expand All @@ -107,64 +68,80 @@ Template.details.events({
});

///////////////////////////////////////////////////////////////////////////////
// Map display
// Party attendance widget

Template.attendance.rsvpName = function () {
var user = Meteor.users.findOne(this.user);
return displayName(user);
};

Template.attendance.outstandingInvitations = function () {
var party = Parties.findOne(this._id);
return Meteor.users.find({$and: [
{_id: {$in: party.invited}}, // they're invited
{_id: {$nin: _.pluck(party.rsvps, 'user')}} // but haven't RSVP'd
]});
};

Template.attendance.invitationName = function () {
return displayName(this);
};

Template.attendance.rsvpIs = function (what) {
return this.rsvp === what;
};

Template.attendance.nobody = function () {
return ! this.public && (this.rsvps.length + this.invited.length === 0);
};

Template.attendance.canInvite = function () {
return ! this.public && this.owner === Meteor.userId();
};

///////////////////////////////////////////////////////////////////////////////
// Map display

// XXX fold into package? domutils?
// http://stackoverflow.com/questions/55677/how-do-i-get-the-coordinates-of-a-mouse-click-on-a-canvas-element
function relMouseCoords (element, event) {
var totalOffsetX = 0;
var totalOffsetY = 0;
var canvasX = 0;
var canvasY = 0;
var currentElement = element;
var coordsRelativeToElement = function (element, event) {
var totalOffsetX = 0, totalOffsetY = 0;

do {
totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft;
totalOffsetY += currentElement.offsetTop - currentElement.scrollTop;
}
while (currentElement = currentElement.offsetParent)

canvasX = event.pageX - totalOffsetX;
canvasY = event.pageY - totalOffsetY;

return {x: canvasX, y: canvasY};
}
totalOffsetX += element.offsetLeft - element.scrollLeft;
totalOffsetY += element.offsetTop - element.scrollTop;
} while (element = element.offsetParent);

return {
x: event.pageX - totalOffsetX,
y: event.pageY - totalOffsetY
};
};

Template.map.events = {
'mousedown circle, mousedown text': function (event, template) {
Session.set("selected", event.currentTarget.id);
},
'dblclick svg': function (event, template) {
// must be logged in
if (!Meteor.userId())
if (! Meteor.userId()) // must be logged in to create events
return;
var coords = relMouseCoords(event.currentTarget, event);
Session.set("createCoords", {
x: coords.x / 500, // XXX event.currentTarget.width?
y: coords.y / 500
});
Session.set("showCreateDialog", true);
var coords = coordsRelativeToElement(event.currentTarget, event);
openCreateDialog(coords.x / 500, coords.y / 500);
}
};

Template.map.rendered = function () {
var self = this;
self.node = this.find("svg");
self.node = self.find("svg");

if (! self.handle) {
self.handle = Meteor.autorun(function () {
var selected = Session.get('selected');
var selectedParty = selected && Parties.findOne(selected);

var radius = function (party) {
return 10 + Math.sqrt(attending(party)) * 10;
};

// Draw a circle for each party
var circles = d3.select(self.node).select(".circles").selectAll("circle")
.data(Parties.find().fetch(), function (party) { return party._id; });

var updateCircles = function (group) {
group.attr("id", function (party) { return party._id; })
.attr("cx", function (party) { return party.x * 500; })
Expand All @@ -178,14 +155,14 @@ Template.map.rendered = function () {
});
};

var circles = d3.select(self.node).select(".circles").selectAll("circle")
.data(Parties.find().fetch(), function (party) { return party._id; });

updateCircles(circles.enter().append("circle"));
updateCircles(circles.transition().duration(250).ease("cubic-out"));
circles.exit().remove();
circles.exit().transition().duration(250).attr("r", 0).remove();

// Label each with the current attendance count
var labels = d3.select(self.node).select(".labels").selectAll("text")
.data(Parties.find().fetch(), function (party) { return party._id; });

var updateLabels = function (group) {
group.attr("id", function (party) { return party._id; })
.text(function (party) {return attending(party) || '';})
Expand All @@ -196,6 +173,9 @@ Template.map.rendered = function () {
});
};

var labels = d3.select(self.node).select(".labels").selectAll("text")
.data(Parties.find().fetch(), function (party) { return party._id; });

updateLabels(labels.enter().append("text"));
updateLabels(labels.transition().duration(250).ease("cubic-out"));
labels.exit().remove();
Expand All @@ -221,16 +201,25 @@ Template.map.destroyed = function () {

///////////////////////////////////////////////////////////////////////////////
// Create Party dialog
///////////////////////////////////////////////////////////////////////////////

var openCreateDialog = function (x, y) {
Session.set("createCoords", {x: x, y: y});
Session.set("createError", null);
Session.set("showCreateDialog", true);
};

Template.page.showCreateDialog = function () {
return Session.get("showCreateDialog");
};

Template.createDialog.events = {
'click .save': function (event, template) {
var title = template.find(".title").value;
var description = template.find(".description").value;
var public = !template.find(".private").checked;
var public = ! template.find(".private").checked;
var coords = Session.get("createCoords");

if (title.length && description.length) {
var coords = Session.get("createCoords");
Meteor.call('createParty', {
title: title,
description: description,
Expand All @@ -241,12 +230,13 @@ Template.createDialog.events = {
if (! error) {
Session.set("selected", party);
if (! public && Meteor.users.find().count() > 1)
Session.set("showInviteDialog", true);
openInviteDialog();
}
});
Session.set("showCreateDialog", false);
} else {
// XXX show validation failure
Session.set("createError",
"It needs a title and a description, or why bother?");
}
},

Expand All @@ -255,9 +245,20 @@ Template.createDialog.events = {
}
};

Template.createDialog.error = function () {
return Session.get("createError");
};

///////////////////////////////////////////////////////////////////////////////
// Invite dialog
///////////////////////////////////////////////////////////////////////////////

var openInviteDialog = function () {
Session.set("showInviteDialog", true);
};

Template.page.showInviteDialog = function () {
return Session.get("showInviteDialog");
};

Template.inviteDialog.events = {
'click .invite': function (event, template) {
Expand All @@ -272,13 +273,9 @@ Template.inviteDialog.events = {
Template.inviteDialog.uninvited = function () {
var party = Parties.findOne(Session.get("selected"));
if (! party)
// XXX this happens on code push when the invite dialog is
// open. easy enough to add a guard, but what's the big picture?
// do we have to do this everywhere?
return [];
var invited = _.clone(party.invited);
invited.push(party.owner);
return Meteor.users.find({_id: {$nin: invited}});
return []; // party hasn't loaded yet
return Meteor.users.find({$nor: [{_id: {$in: party.invited}},
{_id: party.owner}]});
};

Template.inviteDialog.displayName = function () {
Expand Down
2 changes: 1 addition & 1 deletion examples/parties/client/parties.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@ input.chosen {
stroke-opacity: .8;
fill: none;
stroke: red;
}
}
Loading

0 comments on commit 4dee258

Please sign in to comment.