Remove unused JSX pragma in modeling React code
lol again this is hard to test so uhh I hope this didn't break it all!! though tbh I feel like we removed this feature or something anyway? idk it stopped working in some way
This commit is contained in:
parent
2df6ca57cc
commit
90f799faf8
1 changed files with 218 additions and 159 deletions
|
@ -1,20 +1,20 @@
|
||||||
/** @jsx React.DOM */
|
|
||||||
|
|
||||||
var Neopia = (function ($, I18n) {
|
var Neopia = (function ($, I18n) {
|
||||||
// Console-polyfill. MIT license.
|
// Console-polyfill. MIT license.
|
||||||
// https://github.com/paulmillr/console-polyfill
|
// https://github.com/paulmillr/console-polyfill
|
||||||
// Make it safe to do console.log() always.
|
// Make it safe to do console.log() always.
|
||||||
var console = (function (con) {
|
var console = (function (con) {
|
||||||
'use strict';
|
"use strict";
|
||||||
var prop, method;
|
var prop, method;
|
||||||
var empty = {};
|
var empty = {};
|
||||||
var dummy = function () {};
|
var dummy = function () {};
|
||||||
var properties = 'memory'.split(',');
|
var properties = "memory".split(",");
|
||||||
var methods = ('assert,count,debug,dir,dirxml,error,exception,group,' +
|
var methods = (
|
||||||
'groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,' +
|
"assert,count,debug,dir,dirxml,error,exception,group," +
|
||||||
'time,timeEnd,trace,warn').split(',');
|
"groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd," +
|
||||||
while (prop = properties.pop()) con[prop] = con[prop] || empty;
|
"time,timeEnd,trace,warn"
|
||||||
while (method = methods.pop()) con[method] = con[method] || dummy;
|
).split(",");
|
||||||
|
while ((prop = properties.pop())) con[prop] = con[prop] || empty;
|
||||||
|
while ((method = methods.pop())) con[method] = con[method] || dummy;
|
||||||
return con;
|
return con;
|
||||||
})(window.console || {});
|
})(window.console || {});
|
||||||
|
|
||||||
|
@ -24,11 +24,11 @@ var Neopia = (function($, I18n) {
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: Neopia.API_URL + "/users/" + id,
|
url: Neopia.API_URL + "/users/" + id,
|
||||||
useCSRFProtection: false
|
useCSRFProtection: false,
|
||||||
}).then(function (response) {
|
}).then(function (response) {
|
||||||
return response.users[0];
|
return response.users[0];
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
Customization: {
|
Customization: {
|
||||||
request: function (petId, type) {
|
request: function (petId, type) {
|
||||||
|
@ -41,7 +41,7 @@ var Neopia = (function($, I18n) {
|
||||||
type: type,
|
type: type,
|
||||||
url: Neopia.API_URL + "/pets/" + petId + "/customization",
|
url: Neopia.API_URL + "/pets/" + petId + "/customization",
|
||||||
useCSRFProtection: false,
|
useCSRFProtection: false,
|
||||||
data: data
|
data: data,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
get: function (petId) {
|
get: function (petId) {
|
||||||
|
@ -49,53 +49,60 @@ var Neopia = (function($, I18n) {
|
||||||
},
|
},
|
||||||
post: function (petId) {
|
post: function (petId) {
|
||||||
return this.request(petId, "POST");
|
return this.request(petId, "POST");
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
Status: {
|
Status: {
|
||||||
get: function () {
|
get: function () {
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
url: Neopia.API_URL + "/status",
|
url: Neopia.API_URL + "/status",
|
||||||
useCSRFProtection: false
|
useCSRFProtection: false,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
init: function () {
|
init: function () {
|
||||||
var hostEl = $('meta[name=neopia-host]');
|
var hostEl = $("meta[name=neopia-host]");
|
||||||
if (!hostEl.length) {
|
if (!hostEl.length) {
|
||||||
throw "missing neopia-host meta tag";
|
throw "missing neopia-host meta tag";
|
||||||
}
|
}
|
||||||
var host = hostEl.attr('content');
|
var host = hostEl.attr("content");
|
||||||
if (!host) {
|
if (!host) {
|
||||||
throw "neopia-host meta tag exists, but is empty";
|
throw "neopia-host meta tag exists, but is empty";
|
||||||
}
|
}
|
||||||
Neopia.API_URL = "//" + host + "/api/1";
|
Neopia.API_URL = "//" + host + "/api/1";
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var ImpressUser = (function () {
|
var ImpressUser = (function () {
|
||||||
var userSignedIn = ($('meta[name=user-signed-in]').attr('content') === 'true');
|
var userSignedIn =
|
||||||
|
$("meta[name=user-signed-in]").attr("content") === "true";
|
||||||
if (userSignedIn) {
|
if (userSignedIn) {
|
||||||
var currentUserId = $('meta[name=current-user-id]').attr('content');
|
var currentUserId = $("meta[name=current-user-id]").attr("content");
|
||||||
return {
|
return {
|
||||||
addNeopetsUsername: function (username) {
|
addNeopetsUsername: function (username) {
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
url: '/user/' + currentUserId + '/neopets-connections',
|
url: "/user/" + currentUserId + "/neopets-connections",
|
||||||
type: 'POST',
|
type: "POST",
|
||||||
data: {neopets_connection: {neopets_username: username}}
|
data: { neopets_connection: { neopets_username: username } },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
removeNeopetsUsername: function (username) {
|
removeNeopetsUsername: function (username) {
|
||||||
return $.ajax({
|
return $.ajax({
|
||||||
url: '/user/' + currentUserId + '/neopets-connections/' + encodeURIComponent(username),
|
url:
|
||||||
type: 'POST',
|
"/user/" +
|
||||||
data: {_method: 'DELETE'}
|
currentUserId +
|
||||||
|
"/neopets-connections/" +
|
||||||
|
encodeURIComponent(username),
|
||||||
|
type: "POST",
|
||||||
|
data: { _method: "DELETE" },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getNeopetsUsernames: function () {
|
getNeopetsUsernames: function () {
|
||||||
return JSON.parse($('#modeling-neopets-users').attr('data-usernames'));
|
return JSON.parse(
|
||||||
|
$("#modeling-neopets-users").attr("data-usernames")
|
||||||
|
);
|
||||||
},
|
},
|
||||||
id: currentUserId
|
id: currentUserId,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
@ -104,17 +111,21 @@ var Neopia = (function($, I18n) {
|
||||||
localStorage.setItem(this._key, JSON.stringify(usernames));
|
localStorage.setItem(this._key, JSON.stringify(usernames));
|
||||||
},
|
},
|
||||||
addNeopetsUsername: function (username) {
|
addNeopetsUsername: function (username) {
|
||||||
this._setNeopetsUsernames(this.getNeopetsUsernames().concat([username]));
|
this._setNeopetsUsernames(
|
||||||
|
this.getNeopetsUsernames().concat([username])
|
||||||
|
);
|
||||||
},
|
},
|
||||||
removeNeopetsUsername: function (username) {
|
removeNeopetsUsername: function (username) {
|
||||||
this._setNeopetsUsernames(this.getNeopetsUsernames().filter(function(u) {
|
this._setNeopetsUsernames(
|
||||||
|
this.getNeopetsUsernames().filter(function (u) {
|
||||||
return u !== username;
|
return u !== username;
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getNeopetsUsernames: function () {
|
getNeopetsUsernames: function () {
|
||||||
return JSON.parse(localStorage.getItem(this._key)) || [];
|
return JSON.parse(localStorage.getItem(this._key)) || [];
|
||||||
},
|
},
|
||||||
id: null
|
id: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -137,12 +148,14 @@ var Neopia = (function($, I18n) {
|
||||||
var equippedObjectId = closetItems[equippedClosetId].obj_info_id;
|
var equippedObjectId = closetItems[equippedClosetId].obj_info_id;
|
||||||
if (itemsById.hasOwnProperty(equippedObjectId)) {
|
if (itemsById.hasOwnProperty(equippedObjectId)) {
|
||||||
customization.statusByItemId[equippedObjectId] = "success";
|
customization.statusByItemId[equippedObjectId] = "success";
|
||||||
itemsById[equippedObjectId].el.find("span[data-body-id=" +
|
itemsById[equippedObjectId].el
|
||||||
customization.custom_pet.body_id + "]").addClass("modeled")
|
.find("span[data-body-id=" + customization.custom_pet.body_id + "]")
|
||||||
|
.addClass("modeled")
|
||||||
.attr("title", I18n.modeledBodyTitle);
|
.attr("title", I18n.modeledBodyTitle);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this._customizationsByPetId[customization.custom_pet.name] = customization;
|
this._customizationsByPetId[customization.custom_pet.name] =
|
||||||
|
customization;
|
||||||
this._customizations = this._buildCustomizations();
|
this._customizations = this._buildCustomizations();
|
||||||
this._updateCustomizations();
|
this._updateCustomizations();
|
||||||
},
|
},
|
||||||
|
@ -159,22 +172,29 @@ var Neopia = (function($, I18n) {
|
||||||
},
|
},
|
||||||
_createItems: function ($) {
|
_createItems: function ($) {
|
||||||
var itemsById = this._itemsById;
|
var itemsById = this._itemsById;
|
||||||
this._items = $('#newest-unmodeled-items li').map(function() {
|
this._items = $("#newest-unmodeled-items li")
|
||||||
|
.map(function () {
|
||||||
var el = $(this);
|
var el = $(this);
|
||||||
var item = {
|
var item = {
|
||||||
el: el,
|
el: el,
|
||||||
id: el.attr('data-item-id'),
|
id: el.attr("data-item-id"),
|
||||||
name: el.find('h2').text(),
|
name: el.find("h2").text(),
|
||||||
missingBodyIdsPresenceMap: el.find('span[data-body-id]').toArray().reduce(function(map, node) {
|
missingBodyIdsPresenceMap: el
|
||||||
map[$(node).attr('data-body-id')] = true;
|
.find("span[data-body-id]")
|
||||||
|
.toArray()
|
||||||
|
.reduce(function (map, node) {
|
||||||
|
map[$(node).attr("data-body-id")] = true;
|
||||||
return map;
|
return map;
|
||||||
}, {})
|
}, {}),
|
||||||
};
|
};
|
||||||
item.component = React.renderComponent(<ModelForItem item={item} />,
|
item.component = React.renderComponent(
|
||||||
el.find('.models').get(0));
|
<ModelForItem item={item} />,
|
||||||
|
el.find(".models").get(0)
|
||||||
|
);
|
||||||
itemsById[item.id] = item;
|
itemsById[item.id] = item;
|
||||||
return item;
|
return item;
|
||||||
}).toArray();
|
})
|
||||||
|
.toArray();
|
||||||
},
|
},
|
||||||
_loadPetCustomization: function (neopiaPetId) {
|
_loadPetCustomization: function (neopiaPetId) {
|
||||||
return Neopia.Customization.get(neopiaPetId)
|
return Neopia.Customization.get(neopiaPetId)
|
||||||
|
@ -187,9 +207,12 @@ var Neopia = (function($, I18n) {
|
||||||
return neopiaPetIds.map(this._loadPetCustomization.bind(this));
|
return neopiaPetIds.map(this._loadPetCustomization.bind(this));
|
||||||
},
|
},
|
||||||
_loadUserCustomizations: function (neopiaUserId) {
|
_loadUserCustomizations: function (neopiaUserId) {
|
||||||
return Neopia.User.get(neopiaUserId).then(function(neopiaUser) {
|
return Neopia.User.get(neopiaUserId)
|
||||||
|
.then(function (neopiaUser) {
|
||||||
return neopiaUser.links.pets;
|
return neopiaUser.links.pets;
|
||||||
}).then(this._loadManyPetsCustomizations.bind(this)).fail(function() {
|
})
|
||||||
|
.then(this._loadManyPetsCustomizations.bind(this))
|
||||||
|
.fail(function () {
|
||||||
console.error("couldn't load user %s's customizations", neopiaUserId);
|
console.error("couldn't load user %s's customizations", neopiaUserId);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -224,10 +247,12 @@ var Neopia = (function($, I18n) {
|
||||||
init: function ($) {
|
init: function ($) {
|
||||||
Neopia.init();
|
Neopia.init();
|
||||||
this._createItems($);
|
this._createItems($);
|
||||||
var usersEl = $('#modeling-neopets-users');
|
var usersEl = $("#modeling-neopets-users");
|
||||||
if (usersEl.length) {
|
if (usersEl.length) {
|
||||||
this._usersComponent = React.renderComponent(<NeopetsUsernamesForm />,
|
this._usersComponent = React.renderComponent(
|
||||||
usersEl.get(0));
|
<NeopetsUsernamesForm />,
|
||||||
|
usersEl.get(0)
|
||||||
|
);
|
||||||
var usernames = ImpressUser.getNeopetsUsernames();
|
var usernames = ImpressUser.getNeopetsUsernames();
|
||||||
usernames.forEach(this._registerUsername.bind(this));
|
usernames.forEach(this._registerUsername.bind(this));
|
||||||
this._updateUsernames();
|
this._updateUsernames();
|
||||||
|
@ -262,7 +287,7 @@ var Neopia = (function($, I18n) {
|
||||||
this._updateUsernames();
|
this._updateUsernames();
|
||||||
},
|
},
|
||||||
addUsername: function (username) {
|
addUsername: function (username) {
|
||||||
if (typeof this._neopetsUsernamesPresenceMap[username] === 'undefined') {
|
if (typeof this._neopetsUsernamesPresenceMap[username] === "undefined") {
|
||||||
ImpressUser.addNeopetsUsername(username);
|
ImpressUser.addNeopetsUsername(username);
|
||||||
this._registerUsername(username);
|
this._registerUsername(username);
|
||||||
}
|
}
|
||||||
|
@ -274,7 +299,7 @@ var Neopia = (function($, I18n) {
|
||||||
this._updateCustomizations();
|
this._updateCustomizations();
|
||||||
this._updateUsernames();
|
this._updateUsernames();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
var ModelForItem = React.createClass({
|
var ModelForItem = React.createClass({
|
||||||
|
@ -284,11 +309,17 @@ var Neopia = (function($, I18n) {
|
||||||
render: function () {
|
render: function () {
|
||||||
var item = this.props.item;
|
var item = this.props.item;
|
||||||
function createModelPet(customization) {
|
function createModelPet(customization) {
|
||||||
return <ModelPet customization={customization}
|
return (
|
||||||
|
<ModelPet
|
||||||
|
customization={customization}
|
||||||
item={item}
|
item={item}
|
||||||
key={customization.custom_pet.name} />;
|
key={customization.custom_pet.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
var sortedCustomizations = this.state.customizations.slice(0).sort(function(a, b) {
|
var sortedCustomizations = this.state.customizations
|
||||||
|
.slice(0)
|
||||||
|
.sort(function (a, b) {
|
||||||
var aName = a.custom_pet.name.toLowerCase();
|
var aName = a.custom_pet.name.toLowerCase();
|
||||||
var bName = b.custom_pet.name.toLowerCase();
|
var bName = b.custom_pet.name.toLowerCase();
|
||||||
if (aName < bName) return -1;
|
if (aName < bName) return -1;
|
||||||
|
@ -296,7 +327,7 @@ var Neopia = (function($, I18n) {
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
return <ul>{sortedCustomizations.map(createModelPet)}</ul>;
|
return <ul>{sortedCustomizations.map(createModelPet)}</ul>;
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var ModelPet = React.createClass({
|
var ModelPet = React.createClass({
|
||||||
|
@ -304,28 +335,40 @@ var Neopia = (function($, I18n) {
|
||||||
var petName = this.props.customization.custom_pet.name;
|
var petName = this.props.customization.custom_pet.name;
|
||||||
var status = this.props.customization.statusByItemId[this.props.item.id];
|
var status = this.props.customization.statusByItemId[this.props.item.id];
|
||||||
var loadingForItemId = this.props.customization.loadingForItemId;
|
var loadingForItemId = this.props.customization.loadingForItemId;
|
||||||
var disabled = (status === "loading"
|
var disabled = status === "loading" || status === "success";
|
||||||
|| status === "success");
|
if (
|
||||||
if (loadingForItemId !== null && loadingForItemId !== this.props.item.id) {
|
loadingForItemId !== null &&
|
||||||
|
loadingForItemId !== this.props.item.id
|
||||||
|
) {
|
||||||
disabled = true;
|
disabled = true;
|
||||||
}
|
}
|
||||||
var itemName = this.props.item.name;
|
var itemName = this.props.item.name;
|
||||||
var imageSrc = "http://pets.neopets.com/cpn/" + petName + "/1/1.png?" +
|
var imageSrc =
|
||||||
|
"http://pets.neopets.com/cpn/" +
|
||||||
|
petName +
|
||||||
|
"/1/1.png?" +
|
||||||
this.appearanceQuery();
|
this.appearanceQuery();
|
||||||
var title = I18n.pet.title
|
var title = I18n.pet.title
|
||||||
.replace(/%{pet}/g, petName)
|
.replace(/%{pet}/g, petName)
|
||||||
.replace(/%{item}/g, itemName);
|
.replace(/%{item}/g, itemName);
|
||||||
var statusMessage = I18n.pet.status[status] || "";
|
var statusMessage = I18n.pet.status[status] || "";
|
||||||
return <li data-status={status}><button onClick={this.handleClick} title={title} disabled={disabled}>
|
return (
|
||||||
|
<li data-status={status}>
|
||||||
|
<button onClick={this.handleClick} title={title} disabled={disabled}>
|
||||||
<img src={imageSrc} />
|
<img src={imageSrc} />
|
||||||
<div>
|
<div>
|
||||||
<span className="pet-name">{petName}</span>
|
<span className="pet-name">{petName}</span>
|
||||||
<span className="message">{statusMessage}</span>
|
<span className="message">{statusMessage}</span>
|
||||||
</div>
|
</div>
|
||||||
</button></li>;
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleClick: function (e) {
|
handleClick: function (e) {
|
||||||
Modeling.model(this.props.customization.custom_pet.name, this.props.item.id);
|
Modeling.model(
|
||||||
|
this.props.customization.custom_pet.name,
|
||||||
|
this.props.item.id
|
||||||
|
);
|
||||||
},
|
},
|
||||||
appearanceQuery: function () {
|
appearanceQuery: function () {
|
||||||
// By appending this string to the image URL, we update it when and only
|
// By appending this string to the image URL, we update it when and only
|
||||||
|
@ -336,15 +379,20 @@ var Neopia = (function($, I18n) {
|
||||||
assetIdByZone[zone] = biologyByZone[zone].part_id;
|
assetIdByZone[zone] = biologyByZone[zone].part_id;
|
||||||
});
|
});
|
||||||
var equippedByZone = this.props.customization.custom_pet.equipped_by_zone;
|
var equippedByZone = this.props.customization.custom_pet.equipped_by_zone;
|
||||||
var equippedAssetIds = Object.keys(equippedByZone).forEach(function(zone) {
|
var equippedAssetIds = Object.keys(equippedByZone).forEach(function (
|
||||||
|
zone
|
||||||
|
) {
|
||||||
assetIdByZone[zone] = equippedByZone[zone].asset_id;
|
assetIdByZone[zone] = equippedByZone[zone].asset_id;
|
||||||
});
|
});
|
||||||
// Sort the zones, so the string (which should match exactly when the
|
// Sort the zones, so the string (which should match exactly when the
|
||||||
// appearance matches) isn't dependent on iteration order.
|
// appearance matches) isn't dependent on iteration order.
|
||||||
return Object.keys(assetIdByZone).sort().map(function(zone) {
|
return Object.keys(assetIdByZone)
|
||||||
|
.sort()
|
||||||
|
.map(function (zone) {
|
||||||
return "zone[" + zone + "]=" + assetIdByZone[zone];
|
return "zone[" + zone + "]=" + assetIdByZone[zone];
|
||||||
}).join("&");
|
})
|
||||||
}
|
.join("&");
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var NeopetsUsernamesForm = React.createClass({
|
var NeopetsUsernamesForm = React.createClass({
|
||||||
|
@ -355,13 +403,20 @@ var Neopia = (function($, I18n) {
|
||||||
function buildUsernameItem(username) {
|
function buildUsernameItem(username) {
|
||||||
return <NeopetsUsernameItem username={username} key={username} />;
|
return <NeopetsUsernameItem username={username} key={username} />;
|
||||||
}
|
}
|
||||||
return <div>
|
return (
|
||||||
|
<div>
|
||||||
<ul>{this.state.usernames.slice(0).sort().map(buildUsernameItem)}</ul>
|
<ul>{this.state.usernames.slice(0).sort().map(buildUsernameItem)}</ul>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<input type="text" placeholder={I18n.neopetsUsernamesForm.label}
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={I18n.neopetsUsernamesForm.label}
|
||||||
onChange={this.handleChange}
|
onChange={this.handleChange}
|
||||||
value={this.state.newUsername} />
|
value={this.state.newUsername}
|
||||||
<button type="submit">{I18n.neopetsUsernamesForm.submit}</button></form></div>;
|
/>
|
||||||
|
<button type="submit">{I18n.neopetsUsernamesForm.submit}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleChange: function (e) {
|
handleChange: function (e) {
|
||||||
this.setState({ newUsername: e.target.value });
|
this.setState({ newUsername: e.target.value });
|
||||||
|
@ -373,16 +428,20 @@ var Neopia = (function($, I18n) {
|
||||||
Modeling.addUsername(this.state.newUsername);
|
Modeling.addUsername(this.state.newUsername);
|
||||||
this.setState({ newUsername: "" });
|
this.setState({ newUsername: "" });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var NeopetsUsernameItem = React.createClass({
|
var NeopetsUsernameItem = React.createClass({
|
||||||
render: function () {
|
render: function () {
|
||||||
return <li>{this.props.username} <button onClick={this.handleClick}>×</button></li>
|
return (
|
||||||
|
<li>
|
||||||
|
{this.props.username} <button onClick={this.handleClick}>×</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleClick: function (e) {
|
handleClick: function (e) {
|
||||||
Modeling.removeUsername(this.props.username);
|
Modeling.removeUsername(this.props.username);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Modeling.init($);
|
Modeling.init($);
|
||||||
|
|
Loading…
Reference in a new issue