impress/app/assets/javascripts/modeling.js.jsx

387 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @jsx React.DOM */
var Neopia = {
User: {
get: function(id) {
return $.ajax({
dataType: "json",
url: Neopia.API_URL + "/users/" + id,
useCSRFProtection: false
}).then(function(response) {
return response.users[0];
});
}
},
Customization: {
request: function(petId, type) {
var data = {};
if (ImpressUser.id) {
data.impress_user = ImpressUser.id;
}
return $.ajax({
dataType: "json",
type: type,
url: Neopia.API_URL + "/pets/" + petId + "/customization",
useCSRFProtection: false,
data: data
});
},
get: function(petId) {
return this.request(petId, "GET");
},
post: function(petId) {
return this.request(petId, "POST");
}
},
Status: {
get: function() {
return $.ajax({
dataType: "json",
url: Neopia.API_URL + "/status",
useCSRFProtection: false
});
}
},
init: function() {
var hostEl = $('meta[name=neopia-host]');
if (!hostEl.length) {
throw "missing neopia-host meta tag";
}
var host = hostEl.attr('content');
if (!host) {
throw "neopia-host meta tag exists, but is empty";
}
Neopia.API_URL = "http://" + host + "/api/1";
}
};
(function($, I18n) {
// Console-polyfill. MIT license.
// https://github.com/paulmillr/console-polyfill
// Make it safe to do console.log() always.
var console = (function (con) {
'use strict';
var prop, method;
var empty = {};
var dummy = function() {};
var properties = 'memory'.split(',');
var methods = ('assert,count,debug,dir,dirxml,error,exception,group,' +
'groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,' +
'time,timeEnd,trace,warn').split(',');
while (prop = properties.pop()) con[prop] = con[prop] || empty;
while (method = methods.pop()) con[method] = con[method] || dummy;
return con;
})(window.console || {});
var ImpressUser = (function() {
var userSignedIn = ($('meta[name=user-signed-in]').attr('content') === 'true');
if (userSignedIn) {
var currentUserId = $('meta[name=current-user-id]').attr('content');
return {
addNeopetsUsername: function(username) {
return $.ajax({
url: '/user/' + currentUserId + '/neopets-connections',
type: 'POST',
data: {neopets_connection: {neopets_username: username}}
});
},
removeNeopetsUsername: function(username) {
return $.ajax({
url: '/user/' + currentUserId + '/neopets-connections/' + encodeURIComponent(username),
type: 'POST',
data: {_method: 'DELETE'}
});
},
getNeopetsUsernames: function() {
return JSON.parse($('#modeling-neopets-users').attr('data-usernames'));
},
id: currentUserId
};
} else {
return {
_key: "guestNeopetsUsernames",
_setNeopetsUsernames: function(usernames) {
localStorage.setItem(this._key, JSON.stringify(usernames));
},
addNeopetsUsername: function(username) {
this._setNeopetsUsernames(this.getNeopetsUsernames().concat([username]));
},
removeNeopetsUsername: function(username) {
this._setNeopetsUsernames(this.getNeopetsUsernames().filter(function(u) {
return u !== username;
}));
},
getNeopetsUsernames: function() {
return JSON.parse(localStorage.getItem(this._key)) || [];
},
id: null
};
}
})();
var Modeling = {
_customizationsByPetId: {},
_customizations: [],
_itemsById: {},
_items: [],
_usersComponent: {setState: function() {}},
_neopetsUsernamesPresenceMap: {},
_addCustomization: function(customization) {
// Set all equipped, interesting items' statuses as success and cross
// them off the list.
var itemsById = this._itemsById;
var equippedByZone = customization.custom_pet.equipped_by_zone;
var closetItems = customization.closet_items;
Object.keys(equippedByZone).forEach(function(zoneId) {
var equippedClosetId = equippedByZone[zoneId].closet_obj_id;
var equippedObjectId = closetItems[equippedClosetId].obj_info_id;
if (itemsById.hasOwnProperty(equippedObjectId)) {
customization.statusByItemId[equippedObjectId] = "success";
itemsById[equippedObjectId].el.find("span[data-body-id=" +
customization.custom_pet.body_id + "]").addClass("modeled")
.attr("title", I18n.modeledBodyTitle);
}
});
this._customizationsByPetId[customization.custom_pet.name] = customization;
this._customizations = this._buildCustomizations();
this._updateCustomizations();
},
_addNewCustomization: function(customization) {
customization.loadingForItemId = null;
customization.statusByItemId = {};
this._addCustomization(customization);
},
_buildCustomizations: function() {
var modelCustomizationsByPetId = this._customizationsByPetId;
return Object.keys(modelCustomizationsByPetId).map(function(petId) {
return modelCustomizationsByPetId[petId];
});
},
_createItems: function($) {
var itemsById = this._itemsById;
this._items = $('#newest-unmodeled-items li').map(function() {
var el = $(this);
var item = {
el: el,
id: el.attr('data-item-id'),
name: el.find('h2').text(),
missingBodyIdsPresenceMap: el.find('span[data-body-id]').toArray().reduce(function(map, node) {
map[$(node).attr('data-body-id')] = true;
return map;
}, {})
};
item.component = React.renderComponent(<ModelForItem item={item} />,
el.find('.models').get(0));
itemsById[item.id] = item;
return item;
}).toArray();
},
_loadPetCustomization: function(neopiaPetId) {
return Neopia.Customization.get(neopiaPetId)
.done(this._addNewCustomization.bind(this))
.fail(function() {
console.error("couldn't load pet %s", neopiaPetId);
});
},
_loadManyPetsCustomizations: function(neopiaPetIds) {
return neopiaPetIds.map(this._loadPetCustomization.bind(this));
},
_loadUserCustomizations: function(neopiaUserId) {
return Neopia.User.get(neopiaUserId).then(function(neopiaUser) {
return neopiaUser.links.pets;
}).then(this._loadManyPetsCustomizations.bind(this)).fail(function() {
console.error("couldn't load user %s's customizations", neopiaUserId);
});
},
_startLoading: function(neopiaPetId, itemId) {
var customization = this._customizationsByPetId[neopiaPetId];
customization.loadingForItemId = itemId;
customization.statusByItemId[itemId] = "loading";
this._updateCustomizations();
},
_stopLoading: function(neopiaPetId, itemId, status) {
var customization = this._customizationsByPetId[neopiaPetId];
customization.loadingForItemId = null;
customization.statusByItemId[itemId] = status;
this._updateCustomizations();
},
_updateCustomizations: function() {
var neopetsUsernamesPresenceMap = this._neopetsUsernamesPresenceMap;
var liveCustomizations = this._customizations.filter(function(c) {
return neopetsUsernamesPresenceMap[c.custom_pet.owner];
});
this._items.forEach(function(item) {
var filteredCustomizations = liveCustomizations.filter(function(c) {
return item.missingBodyIdsPresenceMap[c.custom_pet.body_id];
});
item.component.setState({customizations: filteredCustomizations});
});
},
_updateUsernames: function() {
var usernames = Object.keys(this._neopetsUsernamesPresenceMap);
this._usersComponent.setState({usernames: usernames});
},
init: function($) {
Neopia.init();
this._createItems($);
var usersEl = $('#modeling-neopets-users');
this._usersComponent = React.renderComponent(<NeopetsUsernamesForm />,
usersEl.get(0));
var usernames = ImpressUser.getNeopetsUsernames();
usernames.forEach(this._registerUsername.bind(this));
this._updateUsernames();
},
model: function(neopiaPetId, itemId) {
var oldCustomization = this._customizationsByPetId[neopiaPetId];
var itemsById = this._itemsById;
this._startLoading(neopiaPetId, itemId);
return Neopia.Customization.post(neopiaPetId)
.done(function(newCustomization) {
// Add this field as null for consistency.
newCustomization.loadingForItemId = null;
// Copy previous statuses.
newCustomization.statusByItemId = oldCustomization.statusByItemId;
// Set the attempted item's status as unworn (to possibly be
// overridden by the upcoming loop in _addCustomization).
newCustomization.statusByItemId[itemId] = "unworn";
// Now, finally, let's overwrite the old customization with the new.
Modeling._addCustomization(newCustomization);
})
.fail(function() {
Modeling._stopLoading(neopiaPetId, itemId, "error");
});
},
_registerUsername: function(username) {
this._neopetsUsernamesPresenceMap[username] = true;
this._loadUserCustomizations(username);
this._updateUsernames();
},
addUsername: function(username) {
if (typeof this._neopetsUsernamesPresenceMap[username] === 'undefined') {
ImpressUser.addNeopetsUsername(username);
this._registerUsername(username);
}
},
removeUsername: function(username) {
if (this._neopetsUsernamesPresenceMap[username]) {
ImpressUser.removeNeopetsUsername(username);
delete this._neopetsUsernamesPresenceMap[username];
this._updateCustomizations();
this._updateUsernames();
}
}
};
var ModelForItem = React.createClass({
getInitialState: function() {
return {customizations: []};
},
render: function() {
var item = this.props.item;
function createModelPet(customization) {
return <ModelPet customization={customization}
item={item}
key={customization.custom_pet.name} />;
}
var sortedCustomizations = this.state.customizations.slice(0).sort(function(a, b) {
var aName = a.custom_pet.name.toLowerCase();
var bName = b.custom_pet.name.toLowerCase();
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
});
return <ul>{sortedCustomizations.map(createModelPet)}</ul>;
}
});
var ModelPet = React.createClass({
render: function() {
var petName = this.props.customization.custom_pet.name;
var status = this.props.customization.statusByItemId[this.props.item.id];
var loadingForItemId = this.props.customization.loadingForItemId;
var disabled = (status === "loading"
|| status === "success");
if (loadingForItemId !== null && loadingForItemId !== this.props.item.id) {
disabled = true;
}
var itemName = this.props.item.name;
var imageSrc = "http://pets.neopets.com/cpn/" + petName + "/1/1.png?" +
this.appearanceQuery();
var title = I18n.pet.title
.replace(/%{pet}/g, petName)
.replace(/%{item}/g, itemName);
var statusMessage = I18n.pet.status[status] || "";
return <li data-status={status}><button onClick={this.handleClick} title={title} disabled={disabled}>
<img src={imageSrc} />
<div>
<span className="pet-name">{petName}</span>
<span className="message">{statusMessage}</span>
</div>
</button></li>;
},
handleClick: function(e) {
Modeling.model(this.props.customization.custom_pet.name, this.props.item.id);
},
appearanceQuery: function() {
// By appending this string to the image URL, we update it when and only
// when the pet's appearance has changed.
var assetIdByZone = {};
var biologyByZone = this.props.customization.custom_pet.biology_by_zone;
var biologyPartIds = Object.keys(biologyByZone).forEach(function(zone) {
assetIdByZone[zone] = biologyByZone[zone].part_id;
});
var equippedByZone = this.props.customization.custom_pet.equipped_by_zone;
var equippedAssetIds = Object.keys(equippedByZone).forEach(function(zone) {
assetIdByZone[zone] = equippedByZone[zone].asset_id;
});
// Sort the zones, so the string (which should match exactly when the
// appearance matches) isn't dependent on iteration order.
return Object.keys(assetIdByZone).sort().map(function(zone) {
return "zone[" + zone + "]=" + assetIdByZone[zone];
}).join("&");
}
});
var NeopetsUsernamesForm = React.createClass({
getInitialState: function() {
return {usernames: [], newUsername: ""};
},
render: function() {
function buildUsernameItem(username) {
return <NeopetsUsernameItem username={username} key={username} />;
}
return <div>
<ul>{this.state.usernames.slice(0).sort().map(buildUsernameItem)}</ul>
<form onSubmit={this.handleSubmit}>
<input type="text" placeholder={I18n.neopetsUsernamesForm.label}
onChange={this.handleChange}
value={this.state.newUsername} />
<button type="submit">{I18n.neopetsUsernamesForm.submit}</button></form></div>;
},
handleChange: function(e) {
this.setState({newUsername: e.target.value});
},
handleSubmit: function(e) {
e.preventDefault();
this.state.newUsername = $.trim(this.state.newUsername);
if (this.state.newUsername.length) {
Modeling.addUsername(this.state.newUsername);
this.setState({newUsername: ""});
}
}
});
var NeopetsUsernameItem = React.createClass({
render: function() {
return <li>{this.props.username} <button onClick={this.handleClick}>×</button></li>
},
handleClick: function(e) {
Modeling.removeUsername(this.props.username);
}
});
Modeling.init($);
})(jQuery, ModelingI18n);