var Neopia = (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 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 = "//" + host + "/api/1"; }, }; 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( , 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"); if (usersEl.length) { this._usersComponent = React.renderComponent( , 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 ( ); } 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
    {sortedCustomizations.map(createModelPet)}
; }, }); 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 (
  • ); }, 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 ; } return (
      {this.state.usernames.slice(0).sort().map(buildUsernameItem)}
    ); }, 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 (
  • {this.props.username}
  • ); }, handleClick: function (e) { Modeling.removeUsername(this.props.username); }, }); Modeling.init($); return Neopia; })(jQuery, ModelingI18n);