mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
parties: validation + code cleanups
This commit is contained in:
@@ -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 () {
|
||||
@@ -24,7 +16,6 @@ Meteor.startup(function () {
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Party details sidebar
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
Template.details.party = function () {
|
||||
return Parties.findOne(Session.get("selected"));
|
||||
@@ -41,6 +32,44 @@ Template.details.creatorName = function () {
|
||||
return displayName(owner);
|
||||
};
|
||||
|
||||
Template.details.canRemove = function () {
|
||||
return this.owner === Meteor.userId() && attending(this) === 0;
|
||||
};
|
||||
|
||||
Template.details.maybeChosen = function (what) {
|
||||
var myRsvp = _.find(this.rsvps, function (r) {
|
||||
return r.user === Meteor.userId();
|
||||
}) || {};
|
||||
|
||||
return what == myRsvp.rsvp ? "chosen btn-inverse" : "";
|
||||
};
|
||||
|
||||
Template.details.events({
|
||||
'click .rsvp_yes': function () {
|
||||
Meteor.call("rsvp", Session.get("selected"), "yes");
|
||||
return false;
|
||||
},
|
||||
'click .rsvp_maybe': function () {
|
||||
Meteor.call("rsvp", Session.get("selected"), "maybe");
|
||||
return false;
|
||||
},
|
||||
'click .rsvp_no': function () {
|
||||
Meteor.call("rsvp", Session.get("selected"), "no");
|
||||
return false;
|
||||
},
|
||||
'click .invite': function () {
|
||||
openInviteDialog();
|
||||
return false;
|
||||
},
|
||||
'click .remove': function () {
|
||||
Parties.remove(this._id);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Party attendance widget
|
||||
|
||||
Template.attendance.rsvpName = function () {
|
||||
var user = Meteor.users.findOne(this.user);
|
||||
return displayName(user);
|
||||
@@ -48,9 +77,10 @@ Template.attendance.rsvpName = function () {
|
||||
|
||||
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}});
|
||||
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 () {
|
||||
@@ -69,102 +99,49 @@ Template.attendance.canInvite = function () {
|
||||
return ! this.public && this.owner === Meteor.userId();
|
||||
};
|
||||
|
||||
Template.details.canRemove = function () {
|
||||
return this.owner === Meteor.userId() && attending(this) === 0;
|
||||
};
|
||||
|
||||
Template.details.maybeChosen = function (what) {
|
||||
var myRsvp = _.find(this.rsvps, function (r) {
|
||||
return r.user === Meteor.userId();
|
||||
}) || {};
|
||||
|
||||
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;
|
||||
},
|
||||
'click .rsvp_maybe': function () {
|
||||
Meteor.call("rsvp", Session.get("selected"), "maybe");
|
||||
return false;
|
||||
},
|
||||
'click .rsvp_no': function () {
|
||||
Meteor.call("rsvp", Session.get("selected"), "no");
|
||||
return false;
|
||||
},
|
||||
'click .invite': function () {
|
||||
Session.set("showInviteDialog", true);
|
||||
return false;
|
||||
},
|
||||
'click .remove': function () {
|
||||
Parties.remove(this._id);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// 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)
|
||||
totalOffsetX += element.offsetLeft - element.scrollLeft;
|
||||
totalOffsetY += element.offsetTop - element.scrollTop;
|
||||
} while (element = element.offsetParent);
|
||||
|
||||
canvasX = event.pageX - totalOffsetX;
|
||||
canvasY = event.pageY - totalOffsetY;
|
||||
|
||||
return {x: canvasX, y: canvasY};
|
||||
}
|
||||
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; })
|
||||
@@ -178,14 +155,14 @@ Template.map.rendered = function () {
|
||||
});
|
||||
};
|
||||
|
||||
updateCircles(circles.enter().append("circle"));
|
||||
updateCircles(circles.transition().duration(250).ease("cubic-out"));
|
||||
circles.exit().remove();
|
||||
|
||||
// Label each with the current attendance count
|
||||
var labels = d3.select(self.node).select(".labels").selectAll("text")
|
||||
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().transition().duration(250).attr("r", 0).remove();
|
||||
|
||||
// Label each with the current attendance count
|
||||
var updateLabels = function (group) {
|
||||
group.attr("id", function (party) { return party._id; })
|
||||
.text(function (party) {return attending(party) || '';})
|
||||
@@ -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();
|
||||
@@ -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,
|
||||
@@ -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?");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
@@ -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 () {
|
||||
|
||||
@@ -69,4 +69,4 @@ input.chosen {
|
||||
stroke-opacity: .8;
|
||||
fill: none;
|
||||
stroke: red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,60 @@
|
||||
// All Tomorrow's Parties -- data model
|
||||
// Loaded on both the client and the server
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Parties
|
||||
|
||||
/*
|
||||
owner: user id
|
||||
x, y: Number (screen coordinates)
|
||||
title, description: String
|
||||
public: Boolean
|
||||
invited: list of user id's that it's shared with, not including
|
||||
the owner (ignored if public)
|
||||
rsvps: XXX document, fields are 'user' and 'rsvp'
|
||||
Each party is represented by a document in the Parties collection:
|
||||
owner: user id
|
||||
x, y: Number (screen coordinates in the interval [0, 1])
|
||||
title, description: String
|
||||
public: Boolean
|
||||
invited: Array of user id's that are invited (only if !public)
|
||||
rsvps: Array of objects like {user: userId, rsvp: "yes"} (or "no"/"maybe")
|
||||
*/
|
||||
Parties = new Meteor.Collection("parties");
|
||||
|
||||
Parties.allow({
|
||||
insert: function (userId, party) {
|
||||
return false; // use createParty method instead
|
||||
return false; // no cowboy inserts -- use createParty method
|
||||
},
|
||||
update: function (userId, parties, fields, modifier) {
|
||||
return _.all(parties, function (party) {
|
||||
if (userId !== party.owner)
|
||||
return false; // not the owner
|
||||
|
||||
var allowed = ["title", "description", "x", "y"];
|
||||
if (_.difference(fields, allowed).length)
|
||||
return false; // tried to write to forbidden field
|
||||
|
||||
// XXX validate that they keep the right types, and don't write
|
||||
// stupidly long strings to the database. since all we have is a
|
||||
// modifier that take a little logic.
|
||||
|
||||
// A good improvement would be to validate the type of the new
|
||||
// value of the field (and if a string, the length.) In the
|
||||
// future Meteor will have a schema system to makes that easier.
|
||||
return true;
|
||||
});
|
||||
},
|
||||
remove: function (userId, parties) {
|
||||
return ! _.any(parties, function (party) {
|
||||
// deny if not the owner, or if other people are going
|
||||
return party.owner !== userId || attending(party) > 0;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var attending = function (party) {
|
||||
return _.reduce(party.rsvps, function (memo, rsvp) {
|
||||
if (rsvp.rsvp === 'yes')
|
||||
return memo + 1;
|
||||
else
|
||||
return memo;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
var displayName = function (user) {
|
||||
if (user.profile && user.profile.name)
|
||||
return user.profile.name;
|
||||
return user.emails[0].address;
|
||||
};
|
||||
|
||||
var contactEmail = function (user) {
|
||||
if (user.emails && user.emails.length)
|
||||
return user.emails[0].address;
|
||||
if (user.services.facebook && user.services.facebook.email)
|
||||
return user.services.facebook.email;
|
||||
return null;
|
||||
return (_.groupBy(party.rsvps, 'rsvp').yes || []).length;
|
||||
};
|
||||
|
||||
Meteor.methods({
|
||||
// title, description, x, y, public
|
||||
// XXX limit a user to a certain number of parties
|
||||
// options should include: title, description, x, y, public
|
||||
createParty: function (options) {
|
||||
options = options || {};
|
||||
if (! (typeof options.title === "string" && options.title.length &&
|
||||
typeof options.description === "string" &&
|
||||
options.description.length &&
|
||||
typeof options.x === "number" &&
|
||||
options.x >= 0 && options.x <= 1 &&
|
||||
typeof options.y === "number" &&
|
||||
options.y >= 0 && options.y <= 1))
|
||||
// XXX should get rid of the error code
|
||||
typeof options.x === "number" && options.x >= 0 && options.x <= 1 &&
|
||||
typeof options.y === "number" && options.y >= 0 && options.y <= 1))
|
||||
throw new Meteor.Error(400, "Required parameter missing");
|
||||
if (options.title.length > 100)
|
||||
throw new Meteor.Error(413, "Title too long");
|
||||
@@ -104,15 +88,16 @@ Meteor.methods({
|
||||
var from = contactEmail(Meteor.users.findOne(this.userId));
|
||||
var to = contactEmail(Meteor.users.findOne(userId));
|
||||
if (Meteor.isServer && to) {
|
||||
// This code only runs on the server. If you didn't want clients
|
||||
// to be able to see it, you could move it to a separate file.
|
||||
Email.send({
|
||||
from: "noreply@example.com",
|
||||
to: to,
|
||||
replyTo: from || undefined,
|
||||
subject: "PARTY: " + party.title,
|
||||
text:
|
||||
"Hey, I just invited you to '" + party.title + "' on All Tomorrow's Parties.\n" +
|
||||
"\n" +
|
||||
"Come check it out at: " + Meteor.absoluteUrl() + "\n"
|
||||
"Hey, I just invited you to '" + party.title + "' on All Tomorrow's Parties." +
|
||||
"\n\nCome check it out: " + Meteor.absoluteUrl() + "\n"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -126,8 +111,10 @@ Meteor.methods({
|
||||
var party = Parties.findOne(partyId);
|
||||
if (! party)
|
||||
throw new Meteor.Error(404, "No such party");
|
||||
if (! party.public && party.owner !== this.userId && !_.contains(party.invited, this.userId))
|
||||
throw new Meteor.Error(403, "No such party"); // private, but let's not tell this to the user
|
||||
if (! party.public && party.owner !== this.userId &&
|
||||
!_.contains(party.invited, this.userId))
|
||||
// private, but let's not tell this to the user
|
||||
throw new Meteor.Error(403, "No such party");
|
||||
|
||||
var rsvpIndex = _.indexOf(_.pluck(party.rsvps, 'user'), this.userId);
|
||||
if (rsvpIndex !== -1) {
|
||||
@@ -139,18 +126,37 @@ Meteor.methods({
|
||||
{_id: partyId, "rsvps.user": this.userId},
|
||||
{$set: {"rsvps.$.rsvp": rsvp}});
|
||||
} else {
|
||||
// minimongo doesn't yet support $ in modifier. reconstruct
|
||||
// the modifier to be of the form:
|
||||
// {$set: {"rsvps.<index>.rsvp"}}
|
||||
// minimongo doesn't yet support $ in modifier. as a temporary
|
||||
// workaround, make a modifier that uses an index. this is
|
||||
// safe on the client since there's only one thread.
|
||||
var modifier = {$set: {}};
|
||||
modifier.$set["rsvps." + rsvpIndex + ".rsvp"] = rsvp;
|
||||
Parties.update(partyId, modifier);
|
||||
}
|
||||
|
||||
// Possible improvement: send email to the other people that are
|
||||
// coming to the party.
|
||||
} else {
|
||||
// add new rsvp entry
|
||||
Parties.update(
|
||||
partyId,
|
||||
{$push: {rsvps: {user: this.userId, rsvp: rsvp}}});
|
||||
Parties.update(partyId,
|
||||
{$push: {rsvps: {user: this.userId, rsvp: rsvp}}});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Users
|
||||
|
||||
var displayName = function (user) {
|
||||
if (user.profile && user.profile.name)
|
||||
return user.profile.name;
|
||||
return user.emails[0].address;
|
||||
};
|
||||
|
||||
var contactEmail = function (user) {
|
||||
if (user.emails && user.emails.length)
|
||||
return user.emails[0].address;
|
||||
if (user.services && user.services.facebook && user.services.facebook.email)
|
||||
return user.services.facebook.email;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
</body>
|
||||
|
||||
<template name="page">
|
||||
|
||||
{{#if showCreateDialog}}
|
||||
{{> createDialog}}
|
||||
{{/if}}
|
||||
@@ -17,99 +16,38 @@
|
||||
{{> inviteDialog}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="span1"> </div>
|
||||
|
||||
<div class="span10">
|
||||
|
||||
<div class="header row">
|
||||
<div class="span5">
|
||||
<h3 style="margin-bottom: 0px">All Tomorrow's Parties</h3>
|
||||
</div>
|
||||
<div class="span5">
|
||||
<div style="float: right">
|
||||
{{loginButtons align="right"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="span6">
|
||||
{{> map}}
|
||||
{{#if currentUser}}
|
||||
<div class="pagination-centered">
|
||||
<em><small>Double click the map to post a party!</small></em>
|
||||
<div class="header row">
|
||||
<div class="span5">
|
||||
<h3 style="margin-bottom: 0px">All Tomorrow's Parties</h3>
|
||||
</div>
|
||||
<div class="span5">
|
||||
<div style="float: right">
|
||||
{{loginButtons align="right"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="span4">
|
||||
{{> details}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="span1"> </div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="createDialog">
|
||||
<div class="mask"> </div>
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close cancel">×</button>
|
||||
<h3>Add party</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<label>Title</label>
|
||||
<input type="text" class="title span5">
|
||||
|
||||
<label>Description</label>
|
||||
|
||||
<textarea class="description span5"></textarea>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" class="private">
|
||||
Private party — invitees only
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn cancel">Cancel</a>
|
||||
<a href="#" class="btn btn-primary save">Add party</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="inviteDialog">
|
||||
<div class="mask"> </div>
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close done">×</button>
|
||||
<h3>Invite people</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#each uninvited}}
|
||||
<div class="invite-row">
|
||||
<a href="#" class="btn invite">Invite</a>
|
||||
{{displayName}}
|
||||
</div>
|
||||
{{else}}
|
||||
Everyone on the site has already been invited.
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-primary done">Done</a>
|
||||
<div class="row">
|
||||
<div class="span6">
|
||||
{{> map}}
|
||||
{{#if currentUser}}
|
||||
<div class="pagination-centered">
|
||||
<em><small>Double click the map to post a party!</small></em>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="span4">
|
||||
{{> details}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="span1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -117,7 +55,7 @@
|
||||
<template name="map">
|
||||
<div class="map">
|
||||
{{#constant}}
|
||||
<svg width="480" height="480">
|
||||
<svg width="500" height="500">
|
||||
<circle class="callout" cx=-100 cy=-100></circle>
|
||||
<g class="circles"></g>
|
||||
<g class="labels"></g>
|
||||
@@ -157,8 +95,6 @@
|
||||
<b><a href="#" class="remove">delete this listing</a></b>.
|
||||
</small></div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
{{/with}}
|
||||
{{else}}
|
||||
<h1 class="muted pagination-centered">
|
||||
@@ -172,7 +108,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<template name="attendance">
|
||||
<div class="attendance well well-small">
|
||||
<div class="muted who"><b>Who</b></div>
|
||||
@@ -216,6 +151,63 @@
|
||||
<a href="#" class="btn btn-mini invite">Invite people</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="createDialog">
|
||||
<div class="mask"> </div>
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close cancel">×</button>
|
||||
<h3>Add party</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#if error}}
|
||||
<div class="alert alert-error">{{error}}</div>
|
||||
{{/if}}
|
||||
|
||||
<label>Title</label>
|
||||
<input type="text" class="title span5">
|
||||
|
||||
<label>Description</label>
|
||||
<textarea class="description span5"></textarea>
|
||||
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" class="private">
|
||||
Private party — invitees only
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn cancel">Cancel</a>
|
||||
<a href="#" class="btn btn-primary save">Add party</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="inviteDialog">
|
||||
<div class="mask"> </div>
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close done">×</button>
|
||||
<h3>Invite people</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{{#each uninvited}}
|
||||
<div class="invite-row">
|
||||
<a href="#" class="btn invite">Invite</a>
|
||||
{{displayName}}
|
||||
</div>
|
||||
{{else}}
|
||||
Everyone on the site has already been invited.
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-primary done">Done</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// XXX autopublish warning is printed on each restart. super spammy!
|
||||
// All Tomorrow's Parties -- server
|
||||
|
||||
Meteor.publish("directory", function () {
|
||||
return Meteor.users.find({}, {fields: {emails: 1, profile: 1}});
|
||||
|
||||
Reference in New Issue
Block a user