Merged in develop

This commit is contained in:
Keith Holliday 2018-09-10 09:42:51 -05:00
commit 1c51e62e43
578 changed files with 36458 additions and 33407 deletions

View file

@ -1,14 +1,14 @@
import monk from 'monk';
import nconf from 'nconf';
const migrationName = 'mystery-items-201807.js'; // Update per month
const migrationName = 'mystery-items-201808.js'; // Update per month
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
/*
* Award this month's mystery items to subscribers
*/
const MYSTERY_ITEMS = ['armor_mystery_201807', 'head_mystery_201807'];
const MYSTERY_ITEMS = ['armor_mystery_201808', 'head_mystery_201808'];
const CONNECTION_STRING = nconf.get('MIGRATION_CONNECT_STRING');
let dbUsers = monk(CONNECTION_STRING).get('users', { castIds: false });

View file

@ -1,4 +1,4 @@
let migrationName = '20180801_takeThis.js'; // Update per month
let migrationName = '20180904_takeThis.js'; // Update per month
let authorName = 'Sabe'; // in case script author needs to know when their ...
let authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
@ -15,7 +15,7 @@ function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
migration: {$ne: migrationName},
challenges: {$in: ['081f8912-3526-47d5-984f-f71bbeec77fc']}, // Update per month
challenges: {$in: ['1044ec0c-4a85-48c5-9f36-d51c0c62c7d3']}, // Update per month
};
if (lastId) {

11227
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.56.2",
"version": "4.60.2",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@ -9,9 +9,9 @@
"amazon-payments": "^0.2.7",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"apn": "^2.2.0",
"autoprefixer": "^8.5.0",
"aws-sdk": "^2.239.1",
"apn": "^2.2.0",
"axios": "^0.18.0",
"axios-progress-bar": "^1.2.0",
"babel-core": "^6.26.3",
@ -83,6 +83,7 @@
"rimraf": "^2.4.3",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.2",
"smartbanner.js": "^1.9.1",
"stripe": "^5.9.0",
"superagent": "^3.8.3",
"svg-inline-loader": "^0.8.0",

View file

@ -65,6 +65,12 @@ describe('cron', () => {
expect(analytics.track.callCount).to.equal(1);
});
it('calls analytics when user is sleeping', () => {
user.preferences.sleep = true;
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
describe('end of the month perks', () => {
beforeEach(() => {
user.purchased.plan.customerId = 'subscribedId';
@ -655,76 +661,6 @@ describe('cron', () => {
});
});
describe('user is sleeping', () => {
beforeEach(() => {
user.preferences.sleep = true;
});
it('calls analytics', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(analytics.track.callCount).to.equal(1);
});
it('clears user buffs', () => {
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 1,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('resets all dailies without damaging user', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
let healthBefore = user.stats.hp;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
it('sets isDue for daily', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.be.exist;
});
});
describe('todos', () => {
beforeEach(() => {
let todo = {
@ -846,6 +782,15 @@ describe('cron', () => {
expect(tasksByType.dailys[0].isDue).to.be.false;
});
it('computes isDue when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().toDate();
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].isDue).to.exist;
});
it('computes nextDue', () => {
tasksByType.dailys[0].frequency = 'daily';
tasksByType.dailys[0].everyX = 5;
@ -865,6 +810,13 @@ describe('cron', () => {
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should set tasks completed to false when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should reset task checklist for completed dailys', () => {
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
@ -872,6 +824,14 @@ describe('cron', () => {
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should reset task checklist for completed dailys when user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should reset task checklist for dailys with scheduled misses', () => {
daysMissed = 10;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
@ -884,12 +844,19 @@ describe('cron', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
it('should not do damage for missing a daily when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.equal(hpBefore);
});
it('should not do damage for missing a daily when CRON_SAFE_MODE is set', () => {
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
@ -930,7 +897,7 @@ describe('cron', () => {
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
});
it('should decrement quest progress down for missing a daily', () => {
it('should decrement quest.progress.down for missing a daily', () => {
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@ -939,6 +906,16 @@ describe('cron', () => {
expect(progress.down).to.equal(-1);
});
it('should not decrement quest.progress.down for missing a daily when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let progress = cron({user, tasksByType, daysMissed, analytics});
expect(progress.down).to.equal(0);
});
it('should do damage for only yesterday\'s dailies', () => {
daysMissed = 3;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@ -1017,7 +994,7 @@ describe('cron', () => {
expect(tasksByType.habits[0].counterDown).to.equal(0);
});
it('should reset habit counters even if user is resting in the Inn', () => {
it('should reset habit counters even if user is sleeping', () => {
user.preferences.sleep = true;
tasksByType.habits[0].counterUp = 1;
tasksByType.habits[0].counterDown = 1;
@ -1278,7 +1255,23 @@ describe('cron', () => {
expect(user.achievements.perfect).to.equal(0);
});
it('increments user buffs if all (at least 1) due dailies were completed', () => {
it('gives perfect day buff if all (at least 1) due dailies were completed', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('gives perfect day buff if all (at least 1) due dailies were completed when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
@ -1317,6 +1310,31 @@ describe('cron', () => {
expect(user.stats.buffs.streaks).to.be.false;
});
it('clears buffs if user does not have a perfect day (no due dailys) when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = true;
tasksByType.dailys[0].startDate = moment(new Date()).add({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('clears buffs if user does not have a perfect day (at least one due daily not completed)', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = false;
@ -1341,7 +1359,50 @@ describe('cron', () => {
expect(user.stats.buffs.streaks).to.be.false;
});
it('still grants a perfect day when CRON_SAFE_MODE is set', () => {
it('clears buffs if user does not have a perfect day (at least one due daily not completed) when user is sleeping', () => {
user.preferences.sleep = true;
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('always grants a perfect day buff when CRON_SAFE_MODE is set', () => {
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let previousBuffs = user.stats.buffs.toObject();
cronOverride({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('always grants a perfect day buff when CRON_SAFE_MODE is set when user is sleeping', () => {
user.preferences.sleep = true;
sandbox.stub(nconf, 'get').withArgs('CRON_SAFE_MODE').returns('true');
let cronOverride = requireAgain(pathToCronLib).cron;
daysMissed = 1;
@ -1373,6 +1434,20 @@ describe('cron', () => {
common.statsComputed.restore();
});
it('should not add mp to user when user is sleeping', () => {
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
user.preferences.sleep = true;
let mpBefore = user.stats.mp;
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, {maxMP: 100}));
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.mp).to.equal(mpBefore);
common.statsComputed.restore();
});
it('set user\'s mp to statsComputed.maxMP when user.stats.mp is greater', () => {
const statsComputedRes = common.statsComputed(user);
const stubbedStatsComputed = sinon.stub(common, 'statsComputed');
@ -1514,27 +1589,6 @@ describe('cron', () => {
flagCount: 0,
};
});
xit('does not clear pms under 200', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[lastMessageId]).to.exist;
});
xit('clears pms over 200', () => {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${messageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[messageId]).to.not.exist;
});
});
describe('login incentives', () => {
@ -1568,7 +1622,7 @@ describe('cron', () => {
expect(user.loginIncentives).to.eql(1);
});
it('increments loginIncentives by 1 even if user has Dailies paused', () => {
it('increments loginIncentives by 1 even if user is sleeping', () => {
user.preferences.sleep = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.loginIncentives).to.eql(1);

View file

@ -107,6 +107,25 @@ describe('Password Utilities', () => {
}
});
it('defaults to SHA1 encryption if salt is provided', async () => {
let textPassword = 'mySecretPassword';
let salt = sha1MakeSalt();
let hashedPassword = sha1EncryptPassword(textPassword, salt);
let user = {
auth: {
local: {
hashed_password: hashedPassword,
salt,
passwordHashMethod: '',
},
},
};
let isValidPassword = await compare(user, textPassword);
expect(isValidPassword).to.eql(true);
});
it('throws an error if an invalid hashing method is used', async () => {
try {
await compare({

View file

@ -20,7 +20,7 @@ import { TAVERN_ID } from '../../../../website/common/script/';
import shared from '../../../../website/common';
describe('Group Model', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
let party, questLeader, participatingMember, sleepingParticipatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
@ -48,6 +48,11 @@ describe('Group Model', () => {
party: { _id: party._id },
profile: { name: 'Participating Member' },
});
sleepingParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Sleeping Participating Member' },
preferences: { sleep: true },
});
nonParticipatingMember = new User({
party: { _id: party._id },
profile: { name: 'Non-Participating Member' },
@ -61,6 +66,7 @@ describe('Group Model', () => {
party.save(),
questLeader.save(),
participatingMember.save(),
sleepingParticipatingMember.save(),
nonParticipatingMember.save(),
undecidedMember.save(),
]);
@ -80,6 +86,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@ -175,6 +182,34 @@ describe('Group Model', () => {
expect(party._processBossQuest).to.not.be.called;
expect(Group.prototype._processCollectionQuest).to.be.calledOnce;
});
it('does not call _processBossQuest when user is resting in the inn', async () => {
party.quest.key = 'whale';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
it('does not call _processCollectionQuest when user is resting in the inn', async () => {
party.quest.key = 'evilsanta2';
await party.startQuest(questLeader);
await party.save();
await Group.processQuestProgress(sleepingParticipatingMember, progress);
party = await Group.findOne({_id: party._id});
expect(party._processBossQuest).to.not.be.called;
expect(party._processCollectionQuest).to.not.be.called;
});
});
context('Boss Quests', () => {
@ -216,17 +251,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@ -236,6 +274,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@ -248,17 +287,20 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
updatedNonParticipatingMember,
updatedUndecidedMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
User.findById(nonParticipatingMember._id),
User.findById(undecidedMember._id),
]);
expect(updatedLeader.stats.hp).to.eql(42.5);
expect(updatedParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedSleepingParticipatingMember.stats.hp).to.eql(42.5);
expect(updatedNonParticipatingMember.stats.hp).to.eql(50);
expect(updatedUndecidedMember.stats.hp).to.eql(50);
});
@ -497,9 +539,11 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[party.quest.key]).to.eql(1);
@ -508,6 +552,9 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedParticipatingMember.stats.gp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.achievements.quests[party.quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.stats.exp).to.be.greaterThan(0);
expect(updatedSleepingParticipatingMember.stats.gp).to.be.greaterThan(0);
});
});
});
@ -647,6 +694,7 @@ describe('Group Model', () => {
it('returns an array of members whose quest status set to true', () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
@ -654,6 +702,7 @@ describe('Group Model', () => {
expect(party.getParticipatingQuestMembers()).to.eql([
participatingMember._id,
sleepingParticipatingMember._id,
questLeader._id,
]);
});
@ -756,11 +805,12 @@ describe('Group Model', () => {
it('removes user from group quest', async () => {
party.quest.members = {
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[questLeader._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
party.memberCount = 4;
party.memberCount = 5;
await party.save();
await party.leave(participatingMember);
@ -768,6 +818,7 @@ describe('Group Model', () => {
party = await Group.findOne({_id: party._id});
expect(party.quest.members).to.eql({
[questLeader._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
});
@ -775,6 +826,7 @@ describe('Group Model', () => {
it('deletes a private party when the last member leaves', async () => {
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@ -846,6 +898,7 @@ describe('Group Model', () => {
party.privacy = 'public';
await party.leave(participatingMember);
await party.leave(sleepingParticipatingMember);
await party.leave(questLeader);
await party.leave(nonParticipatingMember);
await party.leave(undecidedMember);
@ -1074,6 +1127,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@ -1130,6 +1184,7 @@ describe('Group Model', () => {
let expectedQuestMembers = {};
expectedQuestMembers[questLeader._id] = true;
expectedQuestMembers[participatingMember._id] = true;
expectedQuestMembers[sleepingParticipatingMember._id] = true;
expect(party.quest.members).to.eql(expectedQuestMembers);
});
@ -1148,12 +1203,18 @@ describe('Group Model', () => {
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(participatingMember.party.quest.completed).to.eql(null);
expect(sleepingParticipatingMember.party.quest.key).to.eql('whale');
expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0);
expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(0);
expect(sleepingParticipatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collectedItems).to.eql(0);
@ -1172,9 +1233,11 @@ describe('Group Model', () => {
it('sends email to participating members that quest has started', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@ -1187,8 +1250,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.have.a.lengthOf(3);
expect(memberIds).to.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
expect(typeOfEmail).to.eql('quest-started');
});
@ -1202,6 +1266,13 @@ describe('Group Model', () => {
questStarted: true,
},
}];
sleepingParticipatingMember.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
options: {
questStarted: true,
},
}];
questLeader.webhooks = [{
type: 'questActivity',
url: 'http://someurl.com',
@ -1210,13 +1281,13 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
expect(questActivityWebhook.send).to.be.calledThrice; // for 3 participating members
let args = questActivityWebhook.send.args[0];
let webhooks = args[0].webhooks;
@ -1226,6 +1297,8 @@ describe('Group Model', () => {
expect(webhooks).to.have.a.lengthOf(1);
if (webhookOwner === questLeader._id) {
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
} else if (webhookOwner === sleepingParticipatingMember._id) {
expect(webhooks[0].id).to.eql(sleepingParticipatingMember.webhooks[0].id);
} else {
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
}
@ -1236,9 +1309,11 @@ describe('Group Model', () => {
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@ -1252,14 +1327,17 @@ describe('Group Model', () => {
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.not.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('does not send email to initiating member', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
sleepingParticipatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
sleepingParticipatingMember.save(),
questLeader.save(),
]);
@ -1271,8 +1349,9 @@ describe('Group Model', () => {
let memberIds = _.map(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(sleepingParticipatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
@ -1281,7 +1360,7 @@ describe('Group Model', () => {
await party.startQuest(nonParticipatingMember);
let members = [questLeader._id, participatingMember._id];
let members = [questLeader._id, participatingMember._id, sleepingParticipatingMember._id];
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
@ -1346,6 +1425,7 @@ describe('Group Model', () => {
party.quest.members = {
[questLeader._id]: true,
[participatingMember._id]: true,
[sleepingParticipatingMember._id]: true,
[nonParticipatingMember._id]: false,
[undecidedMember._id]: null,
};
@ -1368,7 +1448,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
@ -1378,7 +1458,7 @@ describe('Group Model', () => {
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.update.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
@ -1386,7 +1466,7 @@ describe('Group Model', () => {
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(10);
expect(User.update.callCount).to.eql(15); // for 3 users
});
});
@ -1396,17 +1476,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.achievements.quests[quest.key]).to.eql(1);
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
expect(updatedSleepingParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
@ -1433,17 +1515,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
it('gives out super awesome Masterclasser achievement when quests done out of order', async () => {
quest = questScrolls.lostMasterclasser1;
party.quest.key = quest.key;
@ -1470,13 +1554,16 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id).exec(),
User.findById(participatingMember._id).exec(),
User.findById(sleepingParticipatingMember._id).exec(),
]);
expect(updatedLeader.achievements.lostMasterclasser).to.eql(true);
expect(updatedParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
expect(updatedSleepingParticipatingMember.achievements.lostMasterclasser).to.not.eql(true);
});
it('gives xp and gold', async () => {
@ -1485,15 +1572,19 @@ describe('Group Model', () => {
let [
updatedLeader,
updatedParticipatingMember,
updatedSleepingParticipatingMember,
] = await Promise.all([
User.findById(questLeader._id),
User.findById(participatingMember._id),
User.findById(sleepingParticipatingMember._id),
]);
expect(updatedLeader.stats.exp).to.eql(quest.drop.exp);
expect(updatedLeader.stats.gp).to.eql(quest.drop.gp);
expect(updatedParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedParticipatingMember.stats.gp).to.eql(quest.drop.gp);
expect(updatedSleepingParticipatingMember.stats.exp).to.eql(quest.drop.exp);
expect(updatedSleepingParticipatingMember.stats.gp).to.eql(quest.drop.gp);
});
context('drops', () => {
@ -1593,13 +1684,16 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
await party.finishQuest(quest);
expect(User.update).to.be.calledTwice;
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.update).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
it('sets user quest object to a clean state', async () => {
@ -1632,7 +1726,7 @@ describe('Group Model', () => {
},
}];
await Promise.all([participatingMember.save(), questLeader.save()]);
await Promise.all([participatingMember.save(), sleepingParticipatingMember.save(), questLeader.save()]);
await party.finishQuest(quest);

View file

@ -63,45 +63,48 @@ describe('GET /challenges/:challengeId', () => {
context('private guild', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'guild', privacy: 'private'},
members: 1,
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the guild and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the guild', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
expect(chal.group).to.eql({
_id: group._id,
@ -114,53 +117,72 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the guild or challenge', async () => {
await challengeLeader.post(`/groups/${group._id}/leave`);
await challengeLeader.sync();
expect(challengeLeader.guilds).to.be.empty; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
});
});
context('party', () => {
let groupLeader;
let challengeLeader;
let group;
let challenge;
let members;
let user;
let nonMember;
let otherMember;
beforeEach(async () => {
user = await generateUser();
nonMember = await generateUser();
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party'},
members: 1,
groupDetails: {type: 'party', privacy: 'private'},
members: 2,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challenge = await generateChallenge(groupLeader, group);
await members[0].post(`/challenges/${challenge._id}/join`);
await groupLeader.post(`/challenges/${challenge._id}/join`);
challengeLeader = members[0];
otherMember = members[1];
challenge = await generateChallenge(challengeLeader, group);
});
it('fails if user doesn\'t have access to the challenge', async () => {
await expect(user.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
it('fails if user isn\'t in the party and isn\'t challenge leader', async () => {
await expect(nonMember.get(`/challenges/${challenge._id}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('should return challenge data', async () => {
let chal = await members[0].get(`/challenges/${challenge._id}`);
it('returns challenge data for any user in the party', async () => {
let chal = await otherMember.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader.id,
profile: {name: groupLeader.profile.name},
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
expect(chal.group).to.eql({
_id: group._id,
id: group.id,
id: group._id,
categories: [],
name: group.name,
summary: group.name,
@ -169,5 +191,21 @@ describe('GET /challenges/:challengeId', () => {
leader: groupLeader.id,
});
});
it('returns challenge data if challenge leader isn\'t in the party or challenge', async () => {
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let chal = await challengeLeader.get(`/challenges/${challenge._id}`);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: challengeLeader._id,
id: challengeLeader._id,
profile: {name: challengeLeader.profile.name},
});
});
});
});

View file

@ -1,6 +1,7 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
generateChallenge,
translate as t,
} from '../../../../helpers/api-integration/v3';
@ -10,7 +11,7 @@ describe('GET /challenges/:challengeId/members', () => {
let user;
beforeEach(async () => {
user = await generateUser();
user = await generateUser({ balance: 1 });
});
it('validates optional req.query.lastId to be an UUID', async () => {
@ -21,7 +22,7 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if challenge doesn\'t exists', async () => {
it('fails if challenge doesn\'t exist', async () => {
await expect(user.get(`/challenges/${generateUUID()}/members`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@ -29,8 +30,8 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('fails if user doesn\'t have access to the challenge', async () => {
let group = await generateGroup(user);
it('fails if user isn\'t in the private group and isn\'t challenge leader', async () => {
let group = await generateGroup(user, {type: 'party', privacy: 'private'});
let challenge = await generateChallenge(user, group);
let anotherUser = await generateUser();
@ -41,6 +42,27 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('works if user isn\'t in the private group but is challenge leader', async () => {
let populatedGroup = await createAndPopulateGroup({
groupDetails: {type: 'party', privacy: 'private'},
members: 1,
});
let groupLeader = populatedGroup.groupLeader;
let challengeLeader = populatedGroup.members[0];
let challenge = await generateChallenge(challengeLeader, populatedGroup.group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
await challengeLeader.post('/groups/party/leave');
await challengeLeader.sync();
expect(challengeLeader.party._id).to.be.undefined; // check that leaving worked
let res = await challengeLeader.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: {name: groupLeader.profile.name},
});
});
it('works with challenges belonging to public guild', async () => {
let leader = await generateUser({balance: 4});
let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()});

View file

@ -94,16 +94,6 @@ describe('POST /challenges', () => {
});
});
it('returns an error when non-leader member creates a challenge in leaderOnly group', async () => {
await expect(groupMember.post('/challenges', {
group: group._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderChal'),
});
});
it('allows non-leader member to create a challenge', async () => {
let populatedGroup = await createAndPopulateGroup({
members: 1,

View file

@ -46,7 +46,7 @@ describe('POST /challenges/:challengeId/join', () => {
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('returns an error when user doesn\'t have permissions to access the challenge', async () => {
it('returns an error when user isn\'t in the private group and isn\'t challenge leader', async () => {
let unauthorizedUser = await generateUser();
await expect(unauthorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
@ -56,6 +56,16 @@ describe('POST /challenges/:challengeId/join', () => {
});
});
it('succeeds when user isn\'t in the private group but is challenge leader', async () => {
await groupLeader.post(`/challenges/${challenge._id}/leave`);
await groupLeader.post(`/groups/${group._id}/leave`);
await groupLeader.sync();
expect(groupLeader.guilds).to.be.empty; // check that leaving worked
let res = await groupLeader.post(`/challenges/${challenge._id}/join`);
expect(res.name).to.equal(challenge.name);
});
it('returns challenge data', async () => {
let res = await authorizedUser.post(`/challenges/${challenge._id}/join`);

View file

@ -1,6 +1,5 @@
import { IncomingWebhook } from '@slack/client';
import nconf from 'nconf';
import moment from 'moment';
import {
createAndPopulateGroup,
generateUser,
@ -90,32 +89,6 @@ describe('POST /chat', () => {
message: t('chatPrivilegesRevoked'),
});
});
it('returns an error when user is muted with date', async () => {
const userWithChatRevoked = await member.update({
'flags.chatRevoked': true,
'flags.chatRevokedEndDate': moment().add(1, 'days').toDate(),
});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('allows a user to chat after revoked time has passed', async () => {
const userWithChatRevoked = await member.update({
'flags.chatRevoked': true,
'flags.chatRevokedEndDate': moment().subtract(1, 'days').toDate(),
});
const newMessage = await userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const groupMessages = await userWithChatRevoked.get(`/groups/${groupWithChat._id}/chat`);
expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
});
});
context('banned word', () => {

View file

@ -1,6 +1,6 @@
import {
generateUser,
} from '../../../helpers/api-integration/v4';
} from '../../../../helpers/api-integration/v3';
describe('GET /inbox/messages', () => {
let user;
@ -22,17 +22,26 @@ describe('GET /inbox/messages', () => {
message: 'third',
});
// message to yourself
await user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
});
await user.sync();
});
it('returns the user inbox messages as an array of ordered messages (from most to least recent)', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages.length).to.equal(Object.keys(user.inbox.messages).length);
expect(messages.length).to.equal(4);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
// message to yourself
expect(messages[0].text).to.equal('fourth');
expect(messages[0].uuid).to.equal(user._id);
expect(messages[1].text).to.equal('third');
expect(messages[2].text).to.equal('second');
expect(messages[3].text).to.equal('first');
});
});
});

View file

@ -100,7 +100,7 @@ describe('POST /members/send-private-message', () => {
let receiver = await generateUser();
// const initialNotifications = receiver.notifications.length;
await userToSendMessage.post('/members/send-private-message', {
const response = await userToSendMessage.post('/members/send-private-message', {
message: messageToSend,
toUserId: receiver._id,
});
@ -116,6 +116,9 @@ describe('POST /members/send-private-message', () => {
return message.uuid === receiver._id && message.text === messageToSend;
});
expect(response.message.text).to.deep.equal(sendersMessageInSendersInbox.text);
expect(response.message.uuid).to.deep.equal(sendersMessageInSendersInbox.uuid);
// @TODO waiting for mobile support
// expect(updatedReceiver.notifications.length).to.equal(initialNotifications + 1);
// const notification = updatedReceiver.notifications[updatedReceiver.notifications.length - 1];

View file

@ -6,7 +6,7 @@ import {
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none';
let endpoint = '/stripe/subscribe/cancel?noRedirect=true';
let user, group, stripeCancelSubscriptionStub;
beforeEach(async () => {

View file

@ -3,25 +3,41 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('DELETE user message', () => {
let user;
let user, messagesId, otherUser;
beforeEach(async () => {
user = await generateUser({ inbox: { messages: { first: 'message', second: 'message' } } });
expect(user.inbox.messages.first).to.eql('message');
expect(user.inbox.messages.second).to.eql('message');
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
let userRes = await user.get('/user');
messagesId = Object.keys(userRes.inbox.messages);
expect(messagesId.length).to.eql(2);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('first');
expect(userRes.inbox.messages[messagesId[1]].text).to.eql('second');
});
it('one message', async () => {
let result = await user.del('/user/messages/first');
await user.sync();
expect(result).to.eql({ second: 'message' });
expect(user.inbox.messages).to.eql({ second: 'message' });
let result = await user.del(`/user/messages/${messagesId[0]}`);
messagesId = Object.keys(result);
expect(messagesId.length).to.eql(1);
let userRes = await user.get('/user');
expect(Object.keys(userRes.inbox.messages).length).to.eql(1);
expect(userRes.inbox.messages[messagesId[0]].text).to.eql('second');
});
it('clear all', async () => {
let result = await user.del('/user/messages');
await user.sync();
expect(user.inbox.messages).to.eql({});
let userRes = await user.get('/user');
expect(userRes.inbox.messages).to.eql({});
expect(result).to.eql({});
});
});

View file

@ -58,6 +58,21 @@ describe('POST /user/class/cast/:spellId', () => {
});
});
it('returns an error if use Healing Light spell with full health', async () => {
await user.update({
'stats.class': 'healer',
'stats.lvl': 11,
'stats.hp': 50,
'stats.mp': 200,
});
await expect(user.post('/user/class/cast/heal'))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageHealthAlreadyMax'),
});
});
it('returns an error if spell.lvl > user.level', async () => {
await user.update({'stats.mp': 200, 'stats.class': 'wizard'});
await expect(user.post('/user/class/cast/earth'))

View file

@ -50,11 +50,24 @@ describe('POST /user/push-devices', () => {
});
it('adds a push device to the user', async () => {
let response = await user.post('/user/push-devices', {type, regId});
const response = await user.post('/user/push-devices', {type, regId});
await user.sync();
expect(response.message).to.equal(t('pushDeviceAdded'));
expect(response.data[0].type).to.equal(type);
expect(response.data[0].regId).to.equal(regId);
expect(user.pushDevices[0].type).to.equal(type);
expect(user.pushDevices[0].regId).to.equal(regId);
});
it('removes a push device to the user', async () => {
await user.post('/user/push-devices', {type, regId});
const response = await user.del(`/user/push-devices/${regId}`);
await user.sync();
expect(response.message).to.equal(t('pushDeviceRemoved'));
expect(response.data[0]).to.not.exist;
expect(user.pushDevices[0]).to.not.exist;
});
});

View file

@ -0,0 +1,62 @@
import {
generateUser,
translate as t,
} from '../../../helpers/api-integration/v4';
import { v4 as generateUUID } from 'uuid';
describe('DELETE /inbox/messages/:messageId', () => {
let user;
let otherUser;
before(async () => {
[user, otherUser] = await Promise.all([generateUser(), generateUser()]);
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'first',
});
await user.post('/members/send-private-message', {
toUserId: otherUser.id,
message: 'second',
});
await otherUser.post('/members/send-private-message', {
toUserId: user.id,
message: 'third',
});
});
it('returns an error if the messageId parameter is not an UUID', async () => {
await expect(user.del('/inbox/messages/123'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns an error if the message does not exist', async () => {
await expect(user.del(`/inbox/messages/${generateUUID()}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageGroupChatNotFound'),
});
});
it('deletes one message', async () => {
const messages = await user.get('/inbox/messages');
expect(messages.length).to.equal(3);
expect(messages[0].text).to.equal('third');
expect(messages[1].text).to.equal('second');
expect(messages[2].text).to.equal('first');
await user.del(`/inbox/messages/${messages[1]._id}`);
const updatedMessages = await user.get('/inbox/messages');
expect(updatedMessages.length).to.equal(2);
expect(updatedMessages[0].text).to.equal('third');
expect(updatedMessages[1].text).to.equal('first');
});
});

View file

@ -0,0 +1,50 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import NotificationsComponent from 'client/components/notifications.vue';
import Store from 'client/libs/store';
import { hasClass } from 'client/store/getters/members';
const localVue = createLocalVue();
localVue.use(Store);
describe('Notifications', () => {
let store;
beforeEach(() => {
store = new Store({
state: {
user: {
data: {
stats: {
lvl: 0,
},
flags: {},
preferences: {},
party: {
quest: {
},
},
},
},
},
actions: {
'user:fetch': () => {},
'tasks:fetchUserTasks': () => {},
},
getters: {
'members:hasClass': hasClass,
},
});
});
it('set user has class computed prop', () => {
const wrapper = shallowMount(NotificationsComponent, { store, localVue });
expect(wrapper.vm.userHasClass).to.be.false;
store.state.user.data.stats.lvl = 10;
store.state.user.data.flags.classSelected = true;
store.state.user.data.preferences.disableClasses = false;
expect(wrapper.vm.userHasClass).to.be.true;
});
});

View file

@ -1,4 +1,4 @@
import {shallow} from '@vue/test-utils';
import { shallow } from '@vue/test-utils';
import SidebarSection from 'client/components/sidebarSection.vue';
@ -51,4 +51,4 @@ describe('Sidebar Section', () => {
expect(wrapper.find('.section-body').element.style.display).to.eq('none');
});
});
});

38
test/common/ops/spells.js Normal file
View file

@ -0,0 +1,38 @@
import {
generateUser,
} from '../../helpers/common.helper';
import spells from '../../../website/common/script/content/spells';
import {
NotAuthorized,
} from '../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n';
// TODO complete the test suite...
describe('shared.ops.spells', () => {
let user;
beforeEach(() => {
user = generateUser();
});
it('returns an error when healer tries to cast Healing Light with full health', (done) => {
user.stats.class = 'healer';
user.stats.lvl = 11;
user.stats.hp = 50;
user.stats.mp = 200;
let spell = spells.healer.heal;
try {
spell.cast(user);
} catch (err) {
expect(err).to.be.an.instanceof(NotAuthorized);
expect(err.message).to.equal(i18n.t('messageHealthAlreadyMax'));
expect(user.stats.hp).to.eql(50);
expect(user.stats.mp).to.eql(200);
done();
}
});
});

View file

@ -29,7 +29,6 @@ div
buyModal(
:item="selectedItemToBuy || {}",
:withPin="true",
@change="resetItemToBuy($event)",
@buyPressed="customPurchase($event)",
:genericPurchase="genericPurchase(selectedItemToBuy)",
@ -103,7 +102,7 @@ div
<style lang='scss'>
@import '~client/assets/scss/colors.scss';
/* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal {
overflow-y: scroll !important;
@ -567,13 +566,6 @@ export default {
});
}
},
resetItemToBuy ($event) {
// @TODO: Do we need this? I think selecting a new item
// overwrites. @negue might know
if (!$event && this.selectedItemToBuy.purchaseType !== 'card') {
this.selectedItemToBuy = null;
}
},
itemSelected (item) {
this.selectedItemToBuy = item;
},
@ -659,3 +651,4 @@ export default {
<style src="assets/css/sprites/spritesmith-main-21.css"></style>
<style src="assets/css/sprites/spritesmith-main-22.css"></style>
<style src="assets/css/sprites.css"></style>
<style src="smartbanner.js/dist/smartbanner.min.css"></style>

View file

@ -1,54 +1,54 @@
.promo_armoire_backgrounds_201808 {
.promo_animal_tails {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -365px;
background-position: -421px 0px;
width: 141px;
height: 441px;
}
.promo_armoire_backgrounds_201809 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -563px 0px;
width: 141px;
height: 441px;
}
.promo_ember_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -705px 0px;
width: 141px;
height: 441px;
}
.promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -477px 0px;
background-position: 0px -337px;
width: 375px;
height: 361px;
}
.promo_mystery_201807 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -314px -561px;
width: 114px;
height: 120px;
}
.promo_seaserpent {
.promo_kangaroo {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 476px;
height: 364px;
width: 420px;
height: 336px;
}
.promo_mystery_201808 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -847px -462px;
width: 78px;
height: 81px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -429px -561px;
background-position: -847px -392px;
width: 96px;
height: 69px;
}
.promo_unconventional_armor {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -591px -365px;
background-position: -847px -211px;
width: 180px;
height: 180px;
}
.scene_reading {
.scene_perfect_day {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -561px;
width: 171px;
height: 144px;
}
.scene_rewards {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -383px -365px;
width: 207px;
height: 180px;
}
.scene_todos {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -365px;
width: 240px;
height: 195px;
background-position: -847px 0px;
width: 210px;
height: 210px;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 KiB

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 KiB

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View file

@ -6,21 +6,21 @@ b-modal#login-incentives(:title="data.message", size='md', :hide-footer="true")
.row.reward-row
.col-12
avatar.avatar(:member='user', :avatarOnly='true', :withBackground='true')
.text-center.col-12
.text-center.col-12(v-if='nextReward')
.reward-wrap(v-if="!data.rewardText")
div(v-if="nextReward.rewardKey.length === 1", :class="nextReward.rewardKey[0]")
.reward(v-for="reward in nextReward.rewardKey", v-if="nextReward.rewardKey.length > 1", :class='reward')
.reward-wrap(v-if="data.rewardText")
div(v-if="data.rewardKey.length === 1", :class="data.rewardKey[0]")
.reward(v-for="reward in data.rewardKey", v-if="data.rewardKey.length > 1", :class='reward')
.col-12.text-center(v-if="data.nextRewardAt")
.col-12.text-center(v-if="data && data.nextRewardAt")
h4 {{ $t('countLeft', {count: data.nextRewardAt - user.loginIncentives}) }}
.row
.col-12.text-center(v-if='data.rewardText')
p {{ $t('earnedRewardForDevotion', {reward: data.rewardText}) }}
.col-12.text-center
p {{ $t('incentivesDescription') }}
.col-12.text-center(v-if="data.nextRewardAt")
.col-12.text-center(v-if="data && data.nextRewardAt")
h3 {{ $t('nextRewardUnlocksIn', {numberOfCheckinsLeft: data.nextRewardAt - user.loginIncentives}) }}
.modal-footer
.col-12.text-center
@ -70,8 +70,9 @@ export default {
user: 'user.data',
}),
nextReward () {
let nextRewardKey = this.loginIncentives[this.user.loginIncentives].nextRewardAt;
let nextReward = this.loginIncentives[nextRewardKey];
if (!this.loginIncentives[this.user.loginIncentives]) return;
const nextRewardKey = this.loginIncentives[this.user.loginIncentives].nextRewardAt;
const nextReward = this.loginIncentives[nextRewardKey];
return nextReward;
},
},

View file

@ -49,6 +49,8 @@
.social-button {
width: 100%;
height: 100%;
white-space: inherit;
text-align: center;
.text {

View file

@ -21,7 +21,7 @@
.col-12.col-md-6
.btn.btn-secondary.social-button(@click='socialAuth("google")')
.svg-icon.social-icon(v-html="icons.googleIcon")
span {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
.text {{registering ? $t('signUpWithSocial', {social: 'Google'}) : $t('loginWithSocial', {social: 'Google'})}}
.form-group(v-if='registering')
label(for='usernameInput', v-once) {{$t('username')}}
input#usernameInput.form-control(type='text', :placeholder='$t("usernamePlaceholder")', v-model='username')
@ -207,6 +207,8 @@
.social-button {
width: 100%;
height: 100%;
white-space: inherit;
text-align: center;
.text {

View file

@ -15,22 +15,22 @@
// Show avatar only if not currently affected by visual buff
template(v-if="showAvatar()")
span(:class="'chair_' + member.preferences.chair")
span(:class="getGearClass('back')")
span(:class="skinClass")
span(:class="member.preferences.size + '_shirt_' + member.preferences.shirt")
span.head_0
span(:class="member.preferences.size + '_' + getGearClass('armor')")
span(:class="getGearClass('back_collar')")
span(:class="['chair_' + member.preferences.chair, specialMountClass]")
span(:class="[getGearClass('back'), specialMountClass]")
span(:class="[skinClass, specialMountClass]")
span(:class="[member.preferences.size + '_shirt_' + member.preferences.shirt, specialMountClass]")
span(:class="['head_0', specialMountClass]")
span(:class="[member.preferences.size + '_' + getGearClass('armor'), specialMountClass]")
span(:class="[getGearClass('back_collar'), specialMountClass]")
template(v-for="type in ['bangs', 'base', 'mustache', 'beard']")
span(:class="'hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color")
span(:class="getGearClass('body')")
span(:class="getGearClass('eyewear')")
span(:class="getGearClass('head')")
span(:class="getGearClass('headAccessory')")
span(:class="'hair_flower_' + member.preferences.hair.flower")
span(v-if="!hideGear('shield')", :class="getGearClass('shield')")
span(v-if="!hideGear('weapon')", :class="getGearClass('weapon')")
span(:class="['hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color, specialMountClass]")
span(:class="[getGearClass('body'), specialMountClass]")
span(:class="[getGearClass('eyewear'), specialMountClass]")
span(:class="[getGearClass('head'), specialMountClass]")
span(:class="[getGearClass('headAccessory'), specialMountClass]")
span(:class="['hair_flower_' + member.preferences.hair.flower, specialMountClass]")
span(v-if="!hideGear('shield')", :class="[getGearClass('shield'), specialMountClass]")
span(v-if="!hideGear('weapon')", :class="[getGearClass('weapon'), specialMountClass]")
// Resting
span.zzz(v-if="member.preferences.sleep")
@ -67,6 +67,10 @@
bottom: 0px;
left: 0px;
}
.offset-kangaroo {
margin-top: 24px;
}
</style>
<script>
@ -172,6 +176,11 @@ export default {
costumeClass () {
return this.member.preferences.costume ? 'costume' : 'equipped';
},
specialMountClass () {
if (!this.avatarOnly && this.member.items.currentMount && this.member.items.currentMount.indexOf('Kangaroo') !== -1) {
return 'offset-kangaroo';
}
},
},
methods: {
getGearClass (gearType) {

View file

@ -2,7 +2,7 @@
.row
challenge-modal(v-on:updatedChallenge='updatedChallenge')
leave-challenge-modal(:challengeId='challenge._id')
close-challenge-modal(:members='members', :challengeId='challenge._id')
close-challenge-modal(:members='members', :challengeId='challenge._id', :prize='challenge.prize')
challenge-member-progress-modal(:challengeId='challenge._id')
.col-12.col-md-8.standard-page
.row

View file

@ -30,7 +30,7 @@
div.category-wrap(@click.prevent="toggleCategorySelect")
span.category-select(v-if='workingChallenge.categories.length === 0') {{$t('none')}}
.category-label(v-for='category in workingChallenge.categories') {{$t(categoriesHashByKey[category])}}
.category-box(v-if="showCategorySelect")
.category-box(v-if="showCategorySelect && creating")
.form-check(
v-for="group in categoryOptions",
:key="group.key",

View file

@ -74,7 +74,7 @@ div
import memberSearchDropdown from 'client/components/members/memberSearchDropdown';
export default {
props: ['challengeId', 'members'],
props: ['challengeId', 'members', 'prize'],
components: {
memberSearchDropdown,
},
@ -102,7 +102,10 @@ export default {
},
async deleteChallenge () {
if (!confirm('Are you sure you want to delete this challenge?')) return;
this.challenge = await this.$store.dispatch('challenges:deleteChallenge', {challengeId: this.challengeId});
this.challenge = await this.$store.dispatch('challenges:deleteChallenge', {
challengeId: this.challengeId,
prize: this.prize,
});
this.$router.push('/challenges/myChallenges');
},
},

View file

@ -7,30 +7,31 @@ div
h3.leader(
:class='userLevelStyle(msg)',
@click="showMemberModal(msg.uuid)",
v-b-tooltip.hover.top="('contributor' in msg) ? msg.contributor.text : ''",
v-b-tooltip.hover.top="tierTitle",
)
| {{msg.user}}
.svg-icon(v-html="tierIcon", v-if='showShowTierStyle')
p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}}
.text(v-markdown='msg.text')
hr
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
.svg-icon(v-html="icons.like")
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
| {{$t('report')}}
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
span.action.float-right.liked(v-if='likeCount > 0')
.svg-icon(v-html="icons.liked")
| + {{ likeCount }}
div(v-if='msg.id')
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
.svg-icon(v-html="icons.like")
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
| {{$t('report')}}
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
.svg-icon(v-html="icons.delete")
| {{$t('delete')}}
span.action.float-right.liked(v-if='likeCount > 0')
.svg-icon(v-html="icons.liked")
| + {{ likeCount }}
</template>
<style lang="scss" scoped>
@ -118,6 +119,8 @@ import markdownDirective from 'client/directives/markdown';
import { mapState } from 'client/libs/store';
import styleHelper from 'client/mixins/styleHelper';
import achievementsLib from '../../../common/script/libs/achievements';
import deleteIcon from 'assets/svg/delete.svg';
import copyIcon from 'assets/svg/copy.svg';
import likeIcon from 'assets/svg/like.svg';
@ -220,6 +223,10 @@ export default {
}
return this.icons[`tier${message.contributor.level}`];
},
tierTitle () {
const message = this.msg;
return achievementsLib.getContribText(message.contributor, message.backer) || '';
},
},
methods: {
async like () {
@ -254,8 +261,7 @@ export default {
this.$emit('message-removed', message);
if (this.inbox) {
axios.delete(`/api/v4/user/messages/${message.id}`);
this.$delete(this.user.inbox.messages, message.id);
await axios.delete(`/api/v4/inbox/messages/${message.id}`);
return;
}

View file

@ -202,8 +202,17 @@ export default {
if (!profile._id) {
const result = await this.$store.dispatch('members:fetchMember', { memberId });
this.cachedProfileData[memberId] = result.data.data;
profile = result.data.data;
if (result.response && result.response.status === 404) {
return this.$store.dispatch('snackbars:add', {
title: 'Habitica',
text: this.$t('messageDeletedUser'),
type: 'error',
timeout: false,
});
} else {
this.cachedProfileData[memberId] = result.data.data;
profile = result.data.data;
}
}
// Open the modal only if the data is available
@ -221,6 +230,11 @@ export default {
this.chat.splice(chatIndex, 1, message);
},
messageRemoved (message) {
if (this.inbox) {
this.$emit('message-removed', message);
return;
}
const chatIndex = findIndex(this.chat, chatMessage => {
return chatMessage.id === message.id;
});

View file

@ -96,16 +96,10 @@ export default {
};
},
created () {
this.$root.$on('habitica::report-chat', data => {
if (!data.message || !data.groupId) return;
this.abuseObject = data.message;
this.groupId = data.groupId;
this.reportComment = '';
this.$root.$emit('bv::show::modal', 'report-flag');
});
this.$root.$on('habitica::report-chat', this.handleReport);
},
destroyed () {
this.$root.$off('habitica::report-chat');
this.$root.$off('habitica::report-chat', this.handleReport);
},
methods: {
close () {
@ -129,6 +123,13 @@ export default {
});
this.close();
},
handleReport (data) {
if (!data.message || !data.groupId) return;
this.abuseObject = data.message;
this.groupId = data.groupId;
this.reportComment = '';
this.$root.$emit('bv::show::modal', 'report-flag');
},
},
};
</script>

View file

@ -193,8 +193,10 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
.col-3.text-center.sub-menu-item(@click='changeSubPage("flower")', :class='{active: activeSubPage === "flower"}')
strong(v-once) {{$t('accent')}}
.row.sub-menu(v-if='editing')
.col-4.offset-2.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
.col-4.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
strong(v-once) {{$t('animalEars')}}
.col-4.text-center.sub-menu-item(@click='changeSubPage("tails")' :class='{active: activeSubPage === "tails"}')
strong(v-once) {{$t('animalTails')}}
.col-4.text-center.sub-menu-item(@click='changeSubPage("headband")' :class='{active: activeSubPage === "headband"}')
strong(v-once) {{$t('headband')}}
#glasses.row(v-if='activeSubPage === "glasses"')
@ -203,17 +205,30 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
.sprite.customize-option(:class="`eyewear_special_${option.key}`", @click='option.click')
#animal-ears.row(v-if='activeSubPage === "ears"')
.section.col-12.customize-options
.option(v-for='option in animalEars',
.option(v-for='option in animalItems("headAccessory")',
:class='{active: option.active, locked: option.locked}')
.sprite.customize-option(:class="`headAccessory_special_${option.key}`", @click='option.click')
.gem-lock(v-if='option.locked')
.svg-icon.gem(v-html='icons.gem')
span 2
.col-12.text-center(v-if='!animalEarsOwned')
.col-12.text-center(v-if='!animalItemsOwned("headAccessory")')
.gem-lock
.svg-icon.gem(v-html='icons.gem')
span 5
button.btn.btn-secondary.purchase-all(@click='unlock(animalEarsUnlockString)') {{ $t('purchaseAll') }}
button.btn.btn-secondary.purchase-all(@click='unlock(animalItemsUnlockString("headAccessory"))') {{ $t('purchaseAll') }}
#animal-tails.row(v-if='activeSubPage === "tails"')
.section.col-12.customize-options
.option(v-for='option in animalItems("back")',
:class='{active: option.active, locked: option.locked}')
.sprite.customize-option(:class="`icon_back_special_${option.key}`", @click='option.click')
.gem-lock(v-if='option.locked')
.svg-icon.gem(v-html='icons.gem')
span 2
.col-12.text-center(v-if='!animalItemsOwned("back")')
.gem-lock
.svg-icon.gem(v-html='icons.gem')
span 5
button.btn.btn-secondary.purchase-all(@click='unlock(animalItemsUnlockString("back"))') {{ $t('purchaseAll') }}
#headband.row(v-if='activeSubPage === "headband"')
.col-12.customize-options
.option(v-for='option in headbands', :class='{active: option.active}')
@ -222,7 +237,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
.col-12.customize-options
.option(@click='set({"preferences.chair": "none"})', :class='{active: user.preferences.chair === "none"}')
| None
.option(v-for='option in ["black", "blue", "green", "pink", "red", "yellow"]',
.option(v-for='option in chairKeys',
:class='{active: user.preferences.chair === option}')
.chair.sprite.customize-option(:class="`button_chair_${option}`", @click='set({"preferences.chair": option})')
#flowers.row(v-if='activeSubPage === "flower"')
@ -856,7 +871,7 @@ import isPinned from 'common/script/libs/isPinned';
const skinsBySet = groupBy(appearance.skin, 'set.key');
const hairColorBySet = groupBy(appearance.hair.color, 'set.key');
let tasksByCategory = {
const tasksByCategory = {
work: [
{
type: 'habit',
@ -1013,7 +1028,11 @@ export default {
baseHair4Keys: [15, 16, 17, 18, 19, 20],
baseHair5Keys: [1, 2],
baseHair6Keys: [1, 2, 3],
animalEarsKeys: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
animalItemKeys: {
back: ['bearTail', 'cactusTail', 'foxTail', 'lionTail', 'pandaTail', 'pigTail', 'tigerTail', 'wolfTail'],
headAccessory: ['bearEars', 'cactusEars', 'foxEars', 'lionEars', 'pandaEars', 'pigEars', 'tigerEars', 'wolfEars'],
},
chairKeys: ['black', 'blue', 'green', 'pink', 'red', 'yellow', 'handleless_black', 'handleless_blue', 'handleless_green', 'handleless_pink', 'handleless_red', 'handleless_yellow'],
icons: Object.freeze({
logoPurple,
bodyIcon,
@ -1074,44 +1093,6 @@ export default {
});
return options;
},
animalEarsUnlockString () {
let animalItemKeys = this.animalEarsKeys.map(key => {
return `items.gear.owned.headAccessory_special_${key}`;
});
return animalItemKeys.join(',');
},
animalEarsOwned () {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let own = true;
this.animalEarsKeys.forEach(key => {
if (!this.user.items.gear.owned[`headAccessory_special_${key}`]) own = false;
});
return own;
},
animalEars () {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let keys = this.animalEarsKeys;
let options = keys.map(key => {
let newKey = `headAccessory_special_${key}`;
let userPurchased = this.user.items.gear.owned[newKey];
let locked = !userPurchased;
let option = {};
option.key = key;
option.active = this.user.preferences.costume ? this.user.items.gear.costume.headAccessory === newKey : this.user.items.gear.equipped.headAccessory === newKey;
option.locked = locked;
option.click = () => {
let type = this.user.preferences.costume ? 'costume' : 'equipped';
return locked ? this.unlock(`items.gear.owned.${newKey}`) : this.equip(newKey, type);
};
return option;
});
return options;
},
specialShirts () {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
@ -1549,6 +1530,44 @@ export default {
backgroundPurchased () {
this.backgroundUpdate = new Date();
},
animalItemsUnlockString (category) {
const keys = this.animalItemKeys[category].map(key => {
return `items.gear.owned.${category}_special_${key}`;
});
return keys.join(',');
},
animalItemsOwned (category) {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let own = true;
this.animalItemKeys[category].forEach(key => {
if (!this.user.items.gear.owned[`${category}_special_${key}`]) own = false;
});
return own;
},
animalItems (category) {
// @TODO: For some resonse when I use $set on the user purchases object, this is not recomputed. Hack for now
let backgroundUpdate = this.backgroundUpdate; // eslint-disable-line
let keys = this.animalItemKeys[category];
let options = keys.map(key => {
let newKey = `${category}_special_${key}`;
let userPurchased = this.user.items.gear.owned[newKey];
let locked = !userPurchased;
let option = {};
option.key = key;
option.active = this.user.preferences.costume ? this.user.items.gear.costume[category] === newKey : this.user.items.gear.equipped[category] === newKey;
option.locked = locked;
option.click = () => {
let type = this.user.preferences.costume ? 'costume' : 'equipped';
return locked ? this.unlock(`items.gear.owned.${newKey}`) : this.equip(newKey, type);
};
return option;
});
return options;
},
},
};
</script>

View file

@ -62,7 +62,7 @@
p(v-markdown='group.description')
sidebar-section(
:title="$t('challenges')",
:tooltip="isParty ? $t('challengeDetails') : $t('privateDescription')"
:tooltip="$t('challengeDetails')"
)
group-challenges(:groupId='searchId')
div.text-center
@ -478,11 +478,6 @@ export default {
return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId;
});
},
deleteAllMessages () {
if (confirm(this.$t('confirmDeleteAllMessages'))) {
// User.clearPMs();
}
},
checkForAchievements () {
// Checks if user's party has reached 2 players for the first time.
if (!this.user.achievements.partyUp && this.group.memberCount >= 2) {

View file

@ -338,6 +338,9 @@ export default {
// @TOOD: We might not need this since groupId is computed now
this.getMembers();
},
challengeId () {
this.getMembers();
},
group () {
this.getMembers();
},
@ -377,9 +380,7 @@ export default {
this.invites = invites;
}
if (this.$store.state.memberModalOptions.viewingMembers.length > 0) {
this.members = this.$store.state.memberModalOptions.viewingMembers;
}
this.members = this.$store.state.memberModalOptions.viewingMembers;
},
async clickMember (uid, forceShow) {
let user = this.$store.state.user.data;

View file

@ -55,21 +55,10 @@
div(v-if="expandAuth")
pre {{hero.auth}}
.form-group
h5 User Mute Settings
.checkbox
label
input(type='checkbox', v-if='hero.flags', v-model='hero.flags.chatRevoked')
strong Chat Privileges Revoked
div(v-if='hero.flags.chatRevoked && hero.flags.chatRevokedEndDate')
strong User is currently muted until
br
div {{ userRevokedEndDate(hero) }}
div(v-else-if='hero.flags.chatRevoked')
strong User is currently muted indefinitely
div
strong For how many days from today do you want to mute this user? Leave as 0 for indefinite.
br
input(type='number', v-model='numberOfDaysToMute')
.form-group
.checkbox
label
@ -114,10 +103,10 @@
</style>
<script>
import moment from 'moment';
import each from 'lodash/each';
import markdownDirective from 'client/directives/markdown';
import styleHelper from 'client/mixins/styleHelper';
import { mapState } from 'client/libs/store';
import quests from 'common/script/content/quests';
import { mountInfo, petInfo } from 'common/script/content/stable';
@ -126,7 +115,7 @@ import gear from 'common/script/content/gear';
import notifications from 'client/mixins/notifications';
export default {
mixins: [notifications],
mixins: [notifications, styleHelper],
data () {
return {
heroes: [],
@ -143,7 +132,6 @@ export default {
gear,
expandItems: false,
expandAuth: false,
numberOfDaysToMute: 0,
};
},
directives: {
@ -156,10 +144,6 @@ export default {
...mapState({user: 'user.data'}),
},
methods: {
userRevokedEndDate (hero) {
if (moment().isAfter(moment(hero.flags.chatRevokedEndDate))) return 'User is no longer muted';
return moment(hero.flags.chatRevokedEndDate).format(this.user.preferences.dateFormat.toUpperCase());
},
getAllItemPaths () {
// let questsFormat = this.getFormattedItemReference('items.quests', keys(this.quests), 'Numeric Quantity');
// let mountsFormat = this.getFormattedItemReference('items.mounts', keys(this.mountInfo), 'Boolean');
@ -200,11 +184,6 @@ export default {
this.expandAuth = false;
},
async saveHero () {
if (this.numberOfDaysToMute) {
const dayToEndMute = moment().add(this.numberOfDaysToMute, 'days').utc().toDate();
this.hero.flags.chatRevokedEndDate = dayToEndMute;
}
this.hero.contributor.admin = this.hero.contributor.level > 7 ? true : false;
let heroUpdated = await this.$store.dispatch('hall:updateHero', { heroDetails: this.hero });
this.text('User updated');
@ -212,7 +191,6 @@ export default {
this.heroID = -1;
this.heroes[this.currentHeroIndex] = heroUpdated;
this.currentHeroIndex = -1;
this.numberOfDaysToMute = 0;
},
populateContributorInput (id, index) {
this.heroID = id;
@ -226,9 +204,6 @@ export default {
startingPage: 'profile',
});
},
userLevelStyle () {
// @TODO: implement
},
},
};
</script>

View file

@ -35,26 +35,11 @@ div
span.small-text(v-html="$t('inviteFriendsParty')")
br
button.btn.btn-primary(@click='createOrInviteParty()') {{ user.party._id ? $t('inviteFriends') : $t('startAParty') }}
a.useMobileApp(v-if="isAndroidMobile()", v-once, href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica") {{ $t('useMobileApps') }}
a.useMobileApp(v-if="isIOSMobile()", v-once, href="https://itunes.apple.com/us/app/habitica-gamified-task-manager/id994882113?mt=8") {{ $t('useMobileApps') }}
</template>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
.useMobileApp {
background: red;
color: white;
z-index: 10;
width: 100%;
margin: 10px 5px 0 0;
height: 64px;
text-align: center;
display: flex;
align-items: center;
}
#app-header {
padding-left: 24px;
padding-top: 9px;
@ -155,12 +140,6 @@ export default {
...mapActions({
getPartyMembers: 'party:getMembers',
}),
isAndroidMobile () {
return navigator.userAgent.match(/Android/i);
},
isIOSMobile () {
return navigator.userAgent.match(/iPhone|iPad|iPod/i);
},
expandMember (memberId) {
if (this.expandedMember === memberId) {
this.expandedMember = null;
@ -192,12 +171,16 @@ export default {
if (this.user.party && this.user.party._id) {
this.$store.state.memberModalOptions.groupId = this.user.party._id;
this.getPartyMembers();
this.$root.$on('inviteModal::inviteToGroup', (group) => {
this.inviteModalGroup = group;
this.$root.$emit('bv::show::modal', 'invite-modal');
});
}
},
mounted () {
this.$root.$on('inviteModal::inviteToGroup', (group) => {
this.inviteModalGroup = group;
this.$root.$emit('bv::show::modal', 'invite-modal');
});
},
destroyed () {
this.$root.off('inviteModal::inviteToGroup');
},
};
</script>

View file

@ -396,12 +396,13 @@ export default {
toggleUserDropdown () {
this.isUserDropdownOpen = !this.isUserDropdownOpen;
},
sync () {
async sync () {
this.$root.$emit('habitica::resync-requested');
return Promise.all([
await Promise.all([
this.$store.dispatch('user:fetch', {forceLoad: true}),
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
]);
this.$root.$emit('habitica::resync-completed');
},
async getUserGroupPlans () {
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');

View file

@ -405,6 +405,8 @@ export default {
this.currentDraggingEgg = egg;
this.eggClickMode = true;
// Wait for the div.eggInfo.mouse node to be added to the DOM before
// changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
@ -427,6 +429,8 @@ export default {
this.currentDraggingPotion = potion;
this.potionClickMode = true;
// Wait for the div.hatchingPotionInfo.mouse node to be added to the
// DOM before changing its position.
this.$nextTick(() => {
this.mouseMoved(lastMouseMoveEvent);
});
@ -471,6 +475,11 @@ export default {
},
mouseMoved ($event) {
// Keep track of the last mouse position even in click mode so that we
// know where to position the dragged potion/egg info on item click.
lastMouseMoveEvent = $event;
// Update the potion/egg popover if we are already dragging it.
if (this.potionClickMode) {
// dragging potioninfo is 180px wide (90 would be centered)
this.$refs.clickPotionInfo.style.left = `${$event.x - 60}px`;
@ -479,8 +488,6 @@ export default {
// dragging eggInfo is 180px wide (90 would be centered)
this.$refs.clickEggInfo.style.left = `${$event.x - 60}px`;
this.$refs.clickEggInfo.style.top = `${$event.y + 10}px`;
} else {
lastMouseMoveEvent = $event;
}
},
},

View file

@ -17,7 +17,7 @@ div
triggers="hover",
placement="top",
)
h4.popover-content-title {{ item.text() }}
h4.popover-content-title {{ itemName || item.text() }}
div.popover-content-text(v-html="item.notes()")
</template>
@ -45,6 +45,9 @@ export default {
itemContentClass: {
type: String,
},
itemName: {
type: String,
},
active: {
type: Boolean,
},

View file

@ -145,47 +145,17 @@
.btn.btn-flat.btn-show-more(@click="setShowMore(mountGroup.key)", v-if='mountGroup.key !== "specialMounts"')
| {{ $_openedItemRows_isToggled(mountGroup.key) ? $t('showLess') : $t('showMore') }}
drawer(:title="$t('quickInventory')",
:errorMessage="(!hasDrawerTabItems(selectedDrawerTab)) ? ((selectedDrawerTab === 0) ? $t('noFoodAvailable') : $t('noSaddlesAvailable')) : null")
div(slot="drawer-header")
.drawer-tab-container
.drawer-tab.text-right
a.drawer-tab-text(
@click="selectedDrawerTab = 0",
:class="{'drawer-tab-text-active': selectedDrawerTab === 0}",
) {{ drawerTabs[0].label }}
.clearfix
.drawer-tab.float-left
a.drawer-tab-text(
@click="selectedDrawerTab = 1",
:class="{ 'drawer-tab-text-active': selectedDrawerTab === 1 }",
) {{ drawerTabs[1].label }}
#petLikeToEatStable.drawer-help-text(v-once)
| {{ $t('petLikeToEat') + ' ' }}
span.svg-icon.inline.icon-16(v-html="icons.information")
b-popover(
target="petLikeToEatStable"
placement="top"
)
.popover-content-text(v-html="$t('petLikeToEatText')", v-once)
drawer-slider(
:items="drawerTabs[selectedDrawerTab].items",
:scrollButtonsVisible="hasDrawerTabItems(selectedDrawerTab)",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
:itemType="selectedDrawerTab"
)
template(slot="item", slot-scope="context")
foodItem(
:item="context.item",
:itemCount="userItems.food[context.item.key]",
:active="currentDraggingFood == context.item",
@itemDragEnd="onDragEnd()",
@itemDragStart="onDragStart($event, context.item)",
@itemClick="onFoodClicked($event, context.item)"
)
inventoryDrawer
template(slot="item", slot-scope="ctx")
foodItem(
:item="ctx.item",
:itemCount="ctx.itemCount",
:itemContentClass="ctx.itemClass",
:active="currentDraggingFood === ctx.item",
@itemDragEnd="onDragEnd()",
@itemDragStart="onDragStart($event, ctx.item)",
@itemClick="onFoodClicked($event, ctx.item)"
)
hatchedPetDialog(:hideText="true")
div.foodInfo(ref="dragginFoodInfo")
div(v-if="currentDraggingFood != null")
@ -284,9 +254,6 @@
}
}
.drawer-slider .items {
height: 114px;
}
.modal-backdrop.fade.show {
background-color: $purple-50;
@ -386,6 +353,7 @@
import StarBadge from 'client/components/ui/starBadge';
import CountBadge from 'client/components/ui/countBadge';
import DrawerSlider from 'client/components/ui/drawerSlider';
import InventoryDrawer from 'client/components/shared/inventoryDrawer';
import ResizeDirective from 'client/directives/resize.directive';
import DragDropDirective from 'client/directives/dragdrop.directive';
@ -399,6 +367,8 @@
import openedItemRowsMixin from 'client/mixins/openedItemRows';
import petMixin from 'client/mixins/petMixin';
import { CONSTANTS, setLocalSetting, getLocalSetting } from 'client/libs/userlocalManager';
// TODO Normalize special pets and mounts
// import Store from 'client/store';
// import deepFreeze from 'client/libs/deepFreeze';
@ -423,6 +393,7 @@
MountRaisedModal,
WelcomeModal,
HatchingModal,
InventoryDrawer,
},
directives: {
resize: ResizeDirective,
@ -430,13 +401,15 @@
mousePosition: MouseMoveDirective,
},
data () {
const stableSortState = getLocalSetting(CONSTANTS.keyConstants.STABLE_SORT_STATE) || 'standard';
return {
viewOptions: {},
hideMissing: false,
searchText: null,
searchTextThrottled: '',
// sort has the translation-keys as values
selectedSortBy: 'standard',
selectedSortBy: stableSortState,
sortByItems: [
'standard',
'AZ',
@ -461,6 +434,11 @@
let search = this.searchText.toLowerCase();
this.searchTextThrottled = search;
}, 250),
selectedSortBy: {
handler () {
setLocalSetting(CONSTANTS.keyConstants.STABLE_SORT_STATE, this.selectedSortBy);
},
},
},
computed: {
...mapState({
@ -849,11 +827,12 @@
}
},
mouseMoved ($event) {
// Keep track of the last mouse position even in click mode so that we
// know where to position the dragged food icon on click.
lastMouseMoveEvent = $event;
if (this.foodClickMode) {
this.$refs.clickFoodInfo.style.left = `${$event.x - 70}px`;
this.$refs.clickFoodInfo.style.top = `${$event.y}px`;
} else {
lastMouseMoveEvent = $event;
}
},
},

View file

@ -88,7 +88,6 @@ import axios from 'axios';
import moment from 'moment';
import throttle from 'lodash/throttle';
import { toNextLevel } from '../../common/script/statHelpers';
import { shouldDo } from '../../common/script/cron';
import { mapState } from 'client/libs/store';
import notifications from 'client/mixins/notifications';
@ -199,6 +198,9 @@ export default {
userClassSelect () {
return !this.user.flags.classSelected && this.user.stats.lvl >= 10;
},
userHasClass () {
return this.$store.getters['members:hasClass'](this.user);
},
invitedToQuest () {
return this.user.party.quest.RSVPNeeded && !this.user.party.quest.completed;
},
@ -223,12 +225,7 @@ export default {
userExp (after, before) {
if (after === before) return;
if (this.user.stats.lvl === 0) return;
let exp = after - before;
if (exp < -50) { // recalculate exp if user level up
exp = toNextLevel(this.user.stats.lvl - 1) - before + after;
}
this.exp(exp);
this.exp(after - before);
},
userGp (after, before) {
if (after === before) return;
@ -250,9 +247,9 @@ export default {
},
userMp (after, before) {
if (after === before) return;
if (!this.$store.getters['members:hasClass'](this.user)) return;
if (!this.userHasClass) return;
let mana = after - before;
const mana = after - before;
this.mp(mana);
},
userLvl (after, before) {
@ -506,7 +503,7 @@ export default {
case 'CRON':
if (notification.data) {
if (notification.data.hp) this.hp(notification.data.hp, 'hp');
if (notification.data.mp) this.mp(notification.data.mp);
if (notification.data.mp && this.userHasClass) this.mp(notification.data.mp);
}
break;
case 'SCORED_TASK':

View file

@ -40,7 +40,7 @@ export default {
...mapState({user: 'user.data', credentials: 'credentials'}),
getCodesUrl () {
if (!this.user) return '';
return `/api/v4/coupons?_id=${this.user._id}&apiToken=${this.credentials.API_TOKEN}`;
return '/api/v4/coupons';
},
},
methods: {

View file

@ -0,0 +1,169 @@
<template lang="pug">
drawer.inventoryDrawer(
:title="$t('quickInventory')"
:errorMessage="inventoryDrawerErrorMessage(selectedDrawerItemType)"
)
div(slot="drawer-header")
drawer-header-tabs(
:tabs="filteredTabs",
@changedPosition="tabSelected($event)"
)
div(slot="right-item")
#petLikeToEatMarket.drawer-help-text(v-once)
| {{ $t('petLikeToEat') + ' ' }}
span.svg-icon.inline.icon-16(v-html="icons.information")
b-popover(
target="petLikeToEatMarket",
:placement="'top'",
)
.popover-content-text(v-html="$t('petLikeToEatText')", v-once)
drawer-slider(
v-if="hasOwnedItemsForType(selectedDrawerItemType)"
:items="ownedItems(selectedDrawerItemType) || []",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
:itemType="selectedDrawerTab"
)
template(slot="item", slot-scope="ctx")
slot(
name="item",
:item="ctx.item",
:itemClass="getItemClass(selectedDrawerContentType, ctx.item.key)",
:itemCount="userItems[selectedDrawerContentType][ctx.item.key] || 0",
:itemName="getItemName(selectedDrawerItemType, ctx.item)",
:itemType="selectedDrawerItemType"
)
</template>
<script>
import {mapState} from 'client/libs/store';
import inventoryUtils from 'client/mixins/inventoryUtils';
import svgInformation from 'assets/svg/information.svg';
import _filter from 'lodash/filter';
import CountBadge from 'client/components/ui/countBadge';
import Item from 'client/components/inventory/item';
import Drawer from 'client/components/ui/drawer';
import DrawerSlider from 'client/components/ui/drawerSlider';
import DrawerHeaderTabs from 'client/components/ui/drawerHeaderTabs';
export default {
mixins: [inventoryUtils],
components: {
Item,
CountBadge,
Drawer,
DrawerSlider,
DrawerHeaderTabs,
},
props: {
defaultSelectedTab: {
type: Number,
default: 0,
},
showEggs: Boolean,
showPotions: Boolean,
},
data () {
return {
drawerTabs: [
{
key: 'eggs',
label: this.$t('eggs'),
show: () => this.showEggs,
},
{
key: 'food',
label: this.$t('foodTitle'),
show: () => true,
},
{
key: 'hatchingPotions',
label: this.$t('hatchingPotions'),
show: () => this.showPotions,
},
{
key: 'special',
contentType: 'food',
label: this.$t('special'),
show: () => true,
},
],
selectedDrawerTab: this.defaultSelectedTab,
icons: Object.freeze({
information: svgInformation,
}),
};
},
computed: {
...mapState({
content: 'content',
userItems: 'user.data.items',
}),
selectedDrawerItemType () {
return this.filteredTabs[this.selectedDrawerTab].key;
},
selectedDrawerContentType () {
return this.filteredTabs[this.selectedDrawerTab].contentType ||
this.selectedDrawerItemType;
},
filteredTabs () {
return this.drawerTabs.filter(t => t.show());
},
},
methods: {
ownedItems (type) {
let mappedItems = _filter(this.content[type], i => {
return this.userItems[type][i.key] > 0;
});
switch (type) {
case 'food':
return _filter(mappedItems, f => {
return f.key !== 'Saddle';
});
case 'special':
if (this.userItems.food.Saddle) {
return _filter(this.content.food, f => {
return f.key === 'Saddle';
});
} else {
return [];
}
default:
return mappedItems;
}
},
tabSelected ($event) {
this.selectedDrawerTab = $event;
},
hasOwnedItemsForType (type) {
return this.ownedItems(type).length > 0;
},
inventoryDrawerErrorMessage (type) {
if (!this.hasOwnedItemsForType(type)) {
switch (type) {
case 'food': return this.$t('noFoodAvailable');
case 'special': return this.$t('noSaddlesAvailable');
default:
// @TODO: Change any places using similar locales from `pets.json` and use these new locales from 'inventory.json'
return this.$t('noItemsAvailableForType', {type: this.$t(`${type}ItemType`)});
}
}
},
},
};
</script>
<style lang="scss">
.inventoryDrawer {
.drawer-slider {
height: 126px;
}
}
</style>

View file

@ -0,0 +1,126 @@
<template lang="pug">
div.featuredItems
.background(:class="{broken: broken}")
.background(:class="{cracked: broken, broken: broken}")
div.npc
div.featured-label
span.rectangle
span.text {{npcName}}
span.rectangle
div.content
div.featured-label.with-border
span.rectangle
span.text {{ featuredText }}
span.rectangle
div.items.margin-center
shopItem(
v-for="item in featuredItems",
:key="item.key",
:item="item",
:price="item.value",
:itemContentClass="'shop_'+item.key",
:emptyItem="false",
:popoverPosition="'top'",
@click="featuredItemSelected(item)"
)
template(slot="itemBadge", slot-scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
@click.prevent.stop="togglePinned(ctx.item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
</template>
<script>
import ShopItem from './shopItem';
import pinUtils from 'client/mixins/pinUtils';
import svgPin from 'assets/svg/pin.svg';
export default {
mixins: [pinUtils],
props: {
broken: Boolean,
npcName: String,
featuredText: String,
featuredItems: Array,
},
components: {
ShopItem,
},
data () {
return {
icons: Object.freeze({
pin: svgPin,
}),
};
},
methods: {
featuredItemSelected (item) {
this.$emit('featuredItemSelected', item);
},
},
};
</script>
<style lang="scss" scoped>
.featuredItems {
height: 216px;
.background {
width: 100%;
height: 216px;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
z-index: 1; // Always cover background.
}
.npc {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 216px;
}
.background.broken {
background: url('~assets/images/npc/broken/market_broken_background.png');
background-repeat: repeat-x;
}
.background.cracked {
background: url('~assets/images/npc/broken/market_broken_layer.png');
background-repeat: repeat-x;
}
.broken .npc {
background: url('~assets/images/npc/broken/market_broken_npc.png');
background-repeat: no-repeat;
}
}
.featured-label {
margin: 24px auto;
}
@media only screen and (max-width: 768px) {
.featuredItems .content {
display: none !important;
}
}
</style>

View file

@ -11,17 +11,11 @@
<script>
import SecondaryMenu from 'client/components/secondaryMenu';
import notifications from 'client/mixins/notifications';
export default {
mixins: [notifications],
components: {
SecondaryMenu,
},
methods: {
showUnpinNotification (item) {
this.text(this.$t('unpinnedItem', {item: item.text}));
},
},
methods: {},
};
</script>

View file

@ -0,0 +1,52 @@
<template lang="pug">
div
countBadge(
v-if="item.showCount !== false",
:show="true",
:count="count"
)
.badge.badge-pill.badge-purple.gems-left(v-if='item.key === "gem"')
| {{ gemsLeft }}
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': item.pinned, 'hide': !item.pinned}",
@click.prevent.stop="togglePinned(item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
</template>
<script>
import { mapState } from 'client/libs/store';
import CountBadge from 'client/components/ui/countBadge';
import svgPin from 'assets/svg/pin.svg';
import planGemLimits from 'common/script/libs/planGemLimits';
import pinUtils from '../../../mixins/pinUtils';
export default {
mixins: [pinUtils],
props: ['item'],
components: {
CountBadge,
},
data () {
return {
icons: Object.freeze({
pin: svgPin,
}),
};
},
computed: {
...mapState({
user: 'user.data',
userItems: 'user.data.items',
}),
count () {
return this.userItems[this.item.purchaseType][this.item.key];
},
gemsLeft () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
},
},
};
</script>

View file

@ -0,0 +1,94 @@
<template lang="pug">
div.items
shopItem(v-for="item in sortedMarketItems",
:key="item.key",
:item="item",
:emptyItem="false",
:popoverPosition="'top'",
@click="itemSelected(item)")
span(slot="popoverContent")
strong(v-if='item.key === "gem" && gemsLeft === 0') {{ $t('maxBuyGems') }}
h4.popover-content-title {{ item.text }}
template(slot="itemBadge", slot-scope="ctx")
category-item(:item='ctx.item')
</template>
<script>
import { mapState } from 'client/libs/store';
import pinUtils from 'client/mixins/pinUtils';
import planGemLimits from 'common/script/libs/planGemLimits';
import ShopItem from '../shopItem';
import CategoryItem from './categoryItem';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import _map from 'lodash/map';
export default {
mixins: [pinUtils],
props: ['hideLocked', 'hidePinned', 'searchBy', 'sortBy', 'category'],
components: {
CategoryItem,
ShopItem,
},
computed: {
...mapState({
content: 'content',
user: 'user.data',
userItems: 'user.data.items',
userStats: 'user.data.stats',
}),
gemsLeft () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
},
sortedMarketItems () {
let result = _map(this.category.items, (e) => {
return {
...e,
pinned: this.isPinned(e),
};
});
result = _filter(result, (item) => {
if (this.hidePinned && item.pinned) {
return false;
}
if (this.searchBy) {
let foundPosition = item.text.toLowerCase().indexOf(this.searchBy);
if (foundPosition === -1) {
return false;
}
}
return true;
});
switch (this.sortBy) {
case 'AZ': {
result = _sortBy(result, ['text']);
break;
}
case 'sortByNumber': {
result = _sortBy(result, item => {
if (item.showCount === false) return 0;
return this.userItems[item.purchaseType][item.key] || 0;
});
break;
}
}
return result;
},
},
methods: {
itemSelected (item) {
this.$root.$emit('buyModal::showItem', item);
},
},
};
</script>

View file

@ -0,0 +1,167 @@
<template lang="pug">
layout-section(:title="$t('equipment')")
div(slot="filters")
filter-dropdown(
:label="$t('class')",
:initialItem="selectedGearCategory",
:items="marketGearCategories",
:withIcon="true",
@selected="selectedGroupGearByClass = $event.id"
)
span(slot="item", slot-scope="ctx")
span.svg-icon.inline.icon-16(v-html="icons[ctx.item.id]")
span.text {{ getClassName(ctx.item.id) }}
filter-dropdown(
:label="$t('sortBy')",
:initialItem="selectedSortGearBy",
:items="sortGearBy",
@selected="selectedSortGearBy = $event"
)
span(slot="item", slot-scope="ctx")
span.text {{ $t(ctx.item.id) }}
itemRows(
:items="sortedGearItems",
:itemWidth=94,
:itemMargin=24,
:type="'gear'",
:noItemsLabel="$t('noGearItemsOfClass')",
slot="content"
)
template(slot="item", slot-scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
:emptyItem="userItems.gear[ctx.item.key] === undefined",
:popoverPosition="'top'",
@click="gearSelected(ctx.item)"
)
template(slot="itemBadge", slot-scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
@click.prevent.stop="togglePinned(ctx.item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
</template>
<script>
import {mapState} from 'client/libs/store';
import LayoutSection from 'client/components/ui/layoutSection';
import FilterDropdown from 'client/components/ui/filterDropdown';
import ItemRows from 'client/components/ui/itemRows';
import ShopItem from '../shopItem';
import shops from 'common/script/libs/shops';
import svgPin from 'assets/svg/pin.svg';
import svgWarrior from 'assets/svg/warrior.svg';
import svgWizard from 'assets/svg/wizard.svg';
import svgRogue from 'assets/svg/rogue.svg';
import svgHealer from 'assets/svg/healer.svg';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
const sortGearTypes = ['sortByType', 'sortByPrice', 'sortByCon', 'sortByPer', 'sortByStr', 'sortByInt'].map(g => ({id: g}));
const sortGearTypeMap = {
sortByType: 'type',
sortByPrice: 'value',
sortByCon: 'con',
sortByStr: 'str',
sortByInt: 'int',
};
export default {
props: ['hideLocked', 'hidePinned', 'searchBy'],
components: {
LayoutSection,
FilterDropdown,
ItemRows,
ShopItem,
},
data () {
return {
sortGearBy: sortGearTypes,
selectedSortGearBy: sortGearTypes[0],
selectedGroupGearByClass: '',
icons: Object.freeze({
pin: svgPin,
warrior: svgWarrior,
wizard: svgWizard,
rogue: svgRogue,
healer: svgHealer,
}),
};
},
computed: {
...mapState({
content: 'content',
user: 'user.data',
userItems: 'user.data.items',
userStats: 'user.data.stats',
}),
marketGearCategories () {
return shops.getMarketGearCategories(this.user).map(c => {
c.id = c.identifier;
return c;
});
},
selectedGearCategory () {
return this.marketGearCategories.filter(c => c.id === this.selectedGroupGearByClass)[0];
},
sortedGearItems () {
let category = _filter(this.marketGearCategories, ['identifier', this.selectedGroupGearByClass]);
let result = _filter(category[0].items, (gear) => {
if (this.hideLocked && gear.locked) {
return false;
}
if (this.hidePinned && gear.pinned) {
return false;
}
if (this.searchBy) {
let foundPosition = gear.text.toLowerCase().indexOf(this.searchBy);
if (foundPosition === -1) {
return false;
}
}
// hide already owned
return !this.userItems.gear.owned[gear.key];
});
// first all unlocked
// then the selected sort
result = _sortBy(result, [(item) => item.locked, sortGearTypeMap[this.selectedSortGearBy.id]]);
return result;
},
},
methods: {
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
} else {
return this.$t(classType);
}
},
gearSelected (item) {
if (!item.locked) {
this.$root.$emit('buyModal::showItem', item);
}
},
},
created () {
this.selectedGroupGearByClass = this.userStats.class;
},
};
</script>
<style scoped>
</style>

View file

@ -0,0 +1,46 @@
<template lang="pug">
.form
h2(v-once) {{ $t('filter') }}
.form-group
checkbox(
v-for="category in categories",
:key="category.identifier",
:id="`category-${category.identifier}`",
:checked.sync="viewOptions[category.identifier].selected",
:text="category.text"
)
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hideLocked') }}
toggle-switch.float-right(
v-model="lockedChecked",
@change="$emit('update:hideLocked', $event)"
)
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hidePinned') }}
toggle-switch.float-right(
v-model="pinnedChecked",
@change="$emit('update:hidePinned', $event)"
)
</template>
<script>
import Checkbox from 'client/components/ui/checkbox';
import toggleSwitch from 'client/components/ui/toggleSwitch';
export default {
props: ['hidePinned', 'hideLocked', 'categories', 'viewOptions'],
components: {
Checkbox,
toggleSwitch,
},
data () {
return {
lockedChecked: this.hideLocked,
pinnedChecked: this.hidePinned,
};
},
};
</script>
<style scoped>
</style>

View file

@ -1,245 +1,79 @@
<template lang="pug">
.row.market
.standard-sidebar.d-none.d-sm-block
page-layout.market
div(slot="sidebar")
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
.form
h2(v-once) {{ $t('filter') }}
.form-group
.form-check(
v-for="category in categories",
:key="category.identifier",
)
.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="viewOptions[category.identifier].selected", :id="`category-${category.identifier}`")
label.custom-control-label(v-once, :for="`category-${category.identifier}`") {{ category.text }}
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hideLocked') }}
toggle-switch.float-right(
v-model="hideLocked",
)
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hidePinned') }}
toggle-switch.float-right(
v-model="hidePinned",
)
.standard-page
div.featuredItems
.background(:class="{broken: broken}")
.background(:class="{cracked: broken, broken: broken}")
div.npc
div.featured-label
span.rectangle
span.text Alex
span.rectangle
div.content
div.featured-label.with-border
span.rectangle
span.text {{ market.featured.text }}
span.rectangle
div.items.margin-center
shopItem(
v-for="item in market.featured.items",
:key="item.key",
:item="item",
:price="item.value",
:itemContentClass="'shop_'+item.key",
:emptyItem="false",
:popoverPosition="'top'",
@click="featuredItemSelected(item)"
)
template(slot="itemBadge", slot-scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
@click.prevent.stop="togglePinned(ctx.item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
market-filter(
:categories="categories",
:hideLocked.sync="hideLocked",
:hidePinned.sync="hidePinned",
:viewOptions="viewOptions"
)
div(slot="page")
featured-items-header(
:broken="broken",
:npcName="'Alex'",
:featuredText="market.featured.text",
:featuredItems="market.featured.items"
@featuredItemSelected="featuredItemSelected($event)"
)
h1.mb-4.page-header(v-once) {{ $t('market') }}
.clearfix(v-if="viewOptions['equipment'].selected")
h2.float-left.mb-3.filters-title
| {{ $t('equipment') }}
.filters.float-right
span.dropdown-label {{ $t('class') }}
b-dropdown(right=true)
span.dropdown-icon-item(slot="text")
span.svg-icon.inline.icon-16(v-html="icons[selectedGroupGearByClass]")
span.text {{ getClassName(selectedGroupGearByClass) }}
b-dropdown-item(
v-for="gearCategory in marketGearCategories",
@click="selectedGroupGearByClass = gearCategory.identifier",
:active="selectedGroupGearByClass === gearCategory.identifier",
:key="gearCategory.identifier"
)
span.dropdown-icon-item
span.svg-icon.inline.icon-16(v-html="icons[gearCategory.identifier]")
span.text {{ gearCategory.text }}
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortGearBy)", right=true)
b-dropdown-item(
v-for="sort in sortGearBy",
@click="selectedSortGearBy = sort",
:active="selectedSortGearBy === sort",
:key="sort"
) {{ $t(sort) }}
br
itemRows(
:items="filteredGear(selectedGroupGearByClass, searchTextThrottled, selectedSortGearBy, hideLocked, hidePinned)",
:itemWidth=94,
:itemMargin=24,
:type="'gear'",
:noItemsLabel="$t('noGearItemsOfClass')",
v-if="viewOptions['equipment'].selected"
equipment-section(
v-if="viewOptions['equipment'].selected",
:hidePinned="hidePinned",
:hideLocked="hideLocked",
:searchBy="searchTextThrottled"
)
template(slot="item", slot-scope="ctx")
shopItem(
:key="ctx.item.key",
:item="ctx.item",
:emptyItem="userItems.gear[ctx.item.key] === undefined",
:popoverPosition="'top'",
@click="gearSelected(ctx.item)"
layout-section(:title="$t('items')")
div(slot="filters")
filter-dropdown(
:label="$t('sortBy')",
:initialItem="selectedSortItemsBy",
:items="sortItemsBy",
@selected="selectedSortItemsBy = $event"
)
template(slot="itemBadge", slot-scope="ctx")
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
@click.prevent.stop="togglePinned(ctx.item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
.clearfix
h2.float-left.mb-3
| {{ $t('items') }}
div.float-right
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t(selectedSortItemsBy)", right=true)
b-dropdown-item(
v-for="sort in sortItemsBy",
@click="selectedSortItemsBy = sort",
:active="selectedSortItemsBy === sort",
:key="sort"
) {{ $t(sort) }}
span(slot="item", slot-scope="ctx")
span.text {{ $t(ctx.item.id) }}
div(
v-for="category in categories",
v-if="viewOptions[category.identifier].selected && category.identifier !== 'equipment'"
)
h4 {{ category.text }}
div.items
shopItem(
v-for="item in sortedMarketItems(category, selectedSortItemsBy, searchTextThrottled, hidePinned)",
:key="item.key",
:item="item",
:emptyItem="false",
:popoverPosition="'top'",
@click="itemSelected(item)"
category-row(
:hidePinned="hidePinned",
:hideLocked="hideLocked",
:searchBy="searchTextThrottled",
:sortBy="selectedSortItemsBy.id",
:category="category"
)
span(slot="popoverContent")
strong(v-if='item.key === "gem" && gemsLeft === 0') {{ $t('maxBuyGems') }}
h4.popover-content-title {{ item.text }}
template(slot="itemBadge", slot-scope="ctx")
countBadge(
v-if="item.showCount != false",
:show="userItems[item.purchaseType][item.key] != 0",
:count="userItems[item.purchaseType][item.key] || 0"
)
.badge.badge-pill.badge-purple.gems-left(v-if='item.key === "gem"')
| {{ gemsLeft }}
span.badge.badge-pill.badge-item.badge-svg(
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
@click.prevent.stop="togglePinned(ctx.item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
keys-to-kennel(v-if='category.identifier === "special"')
div.fill-height
//- @TODO: Create new InventoryDrawer component and re-use in 'inventory/stable' component.
drawer(
:title="$t('quickInventory')"
:errorMessage="inventoryDrawerErrorMessage(selectedDrawerItemType)"
)
div(slot="drawer-header")
drawer-header-tabs(
:tabs="drawerTabs",
@changedPosition="tabSelected($event)"
)
div(slot="right-item")
#petLikeToEatMarket.drawer-help-text(v-once)
| {{ $t('petLikeToEat') + ' ' }}
span.svg-icon.inline.icon-16(v-html="icons.information")
b-popover(
target="petLikeToEatMarket",
:placement="'top'",
)
.popover-content-text(v-html="$t('petLikeToEatText')", v-once)
drawer-slider(
v-if="hasOwnedItemsForType(selectedDrawerItemType)"
:items="ownedItems(selectedDrawerItemType) || []",
slot="drawer-slider",
:itemWidth=94,
:itemMargin=24,
:itemType="selectedDrawerTab"
)
template(slot="item", slot-scope="ctx")
item(
:item="ctx.item",
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
popoverPosition="top",
@click="selectedItemToSell = ctx.item"
)
template(slot="itemBadge", slot-scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
)
span(slot="popoverContent")
h4.popover-content-title {{ getItemName(selectedDrawerItemType, ctx.item) }}
sellModal(
:item="selectedItemToSell",
:itemType="selectedDrawerItemType",
:itemCount="selectedItemToSell != null ? userItems[drawerTabs[selectedDrawerTab].contentType][selectedItemToSell.key] : 0",
:text="selectedItemToSell != null ? getItemName(selectedDrawerItemType, selectedItemToSell) : ''",
@change="resetItemToSell($event)"
)
inventoryDrawer(:showEggs="true", :showPotions="true")
template(slot="item", slot-scope="ctx")
item.flat(
item(
:item="ctx.item",
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
:showPopover="false"
:itemContentClass="ctx.itemClass",
popoverPosition="top",
@click="sellItem(ctx)"
)
template(slot="itemBadge", slot-scope="ctx")
countBadge(
:show="true",
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
)
countBadge(
slot="itemBadge"
:show="true",
:count="ctx.itemCount"
)
h4.popover-content-title(slot="popoverContent") {{ ctx.itemName }}
sellModal
</template>
<style lang="scss">
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/variables.scss';
.market .drawer-slider {
min-height: 60px;
.message {
top: 10px;
}
}
.fill-height {
height: 38px; // button + margin + padding
}
@ -250,10 +84,6 @@
height: 48px;
}
.featured-label {
margin: 24px auto;
}
.item-wrapper.bordered-item .item {
width: 112px;
height: 112px;
@ -265,43 +95,15 @@
margin: 0 auto;
}
.standard-page {
position: relative;
}
.featuredItems {
height: 216px;
.background {
background: url('~assets/images/npc/#{$npc_market_flavor}/market_background.png');
background-repeat: repeat-x;
width: 100%;
height: 216px;
position: absolute;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
z-index: 1; // Always cover background.
}
.npc {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 216px;
background: url('~assets/images/npc/#{$npc_market_flavor}/market_banner_npc.png');
background-repeat: no-repeat;
@ -312,23 +114,6 @@
left: 80px;
}
}
.background.broken {
background: url('~assets/images/npc/broken/market_broken_background.png');
background-repeat: repeat-x;
}
.background.cracked {
background: url('~assets/images/npc/broken/market_broken_layer.png');
background-repeat: repeat-x;
}
.broken .npc {
background: url('~assets/images/npc/broken/market_broken_npc.png');
background-repeat: no-repeat;
}
}
}
@ -336,20 +121,6 @@
right: -.5em;
top: -.5em;
}
@media only screen and (max-width: 768px) {
.featuredItems .content {
display: none !important;
}
.filters, .filters-title {
float: none;
button {
margin-right: 4em;
margin-bottom: 1em;
}
}
}
</style>
@ -358,14 +129,18 @@
import ShopItem from '../shopItem';
import KeysToKennel from './keysToKennel';
import EquipmentSection from './equipmentSection';
import CategoryRow from './categoryRow';
import Item from 'client/components/inventory/item';
import CountBadge from 'client/components/ui/countBadge';
import Drawer from 'client/components/ui/drawer';
import DrawerSlider from 'client/components/ui/drawerSlider';
import DrawerHeaderTabs from 'client/components/ui/drawerHeaderTabs';
import ItemRows from 'client/components/ui/itemRows';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import Avatar from 'client/components/avatar';
import InventoryDrawer from 'client/components/shared/inventoryDrawer';
import FeaturedItemsHeader from '../featuredItemsHeader';
import PageLayout from 'client/components/ui/pageLayout';
import LayoutSection from 'client/components/ui/layoutSection';
import FilterDropdown from 'client/components/ui/filterDropdown';
import MarketFilter from './filter';
import SellModal from './sellModal.vue';
import EquipmentAttributesGrid from '../../inventory/equipment/attributesGrid.vue';
@ -374,52 +149,45 @@
import svgPin from 'assets/svg/pin.svg';
import svgGem from 'assets/svg/gem.svg';
import svgInformation from 'assets/svg/information.svg';
import svgWarrior from 'assets/svg/warrior.svg';
import svgWizard from 'assets/svg/wizard.svg';
import svgRogue from 'assets/svg/rogue.svg';
import svgHealer from 'assets/svg/healer.svg';
import getItemInfo from 'common/script/libs/getItemInfo';
import isPinned from 'common/script/libs/isPinned';
import shops from 'common/script/libs/shops';
import planGemLimits from 'common/script/libs/planGemLimits';
import _filter from 'lodash/filter';
import _sortBy from 'lodash/sortBy';
import _map from 'lodash/map';
import _throttle from 'lodash/throttle';
const sortGearTypes = ['sortByType', 'sortByPrice', 'sortByCon', 'sortByPer', 'sortByStr', 'sortByInt'];
const sortItems = ['AZ', 'sortByNumber'].map(g => ({id: g}));
import notifications from 'client/mixins/notifications';
import buyMixin from 'client/mixins/buy';
import currencyMixin from '../_currencyMixin';
const sortGearTypeMap = {
sortByType: 'type',
sortByPrice: 'value',
sortByCon: 'con',
sortByStr: 'str',
sortByInt: 'int',
};
import inventoryUtils from 'client/mixins/inventoryUtils';
import pinUtils from 'client/mixins/pinUtils';
export default {
mixins: [notifications, buyMixin, currencyMixin],
mixins: [notifications, buyMixin, currencyMixin, inventoryUtils, pinUtils],
components: {
ShopItem,
KeysToKennel,
Item,
CountBadge,
Drawer,
DrawerSlider,
DrawerHeaderTabs,
ItemRows,
toggleSwitch,
SellModal,
EquipmentAttributesGrid,
Avatar,
InventoryDrawer,
FeaturedItemsHeader,
PageLayout,
LayoutSection,
FilterDropdown,
EquipmentSection,
CategoryRow,
MarketFilter,
SelectMembersModal,
},
watch: {
@ -438,24 +206,10 @@ export default {
pin: svgPin,
gem: svgGem,
information: svgInformation,
warrior: svgWarrior,
wizard: svgWizard,
rogue: svgRogue,
healer: svgHealer,
}),
selectedDrawerTab: 0,
selectedDrawerItemType: 'eggs',
selectedGroupGearByClass: '',
sortGearBy: sortGearTypes,
selectedSortGearBy: 'sortByType',
sortItemsBy: ['AZ', 'sortByNumber'],
selectedSortItemsBy: 'AZ',
selectedItemToSell: null,
sortItemsBy: sortItems,
selectedSortItemsBy: sortItems[0],
hideLocked: false,
hidePinned: false,
@ -474,120 +228,79 @@ export default {
userStats: 'user.data.stats',
userItems: 'user.data.items',
}),
marketGearCategories () {
return shops.getMarketGearCategories(this.user);
},
market () {
return shops.getMarketShop(this.user);
},
categories () {
if (this.market) {
let categories = [
...this.market.categories,
];
if (!this.market) return [];
categories.push({
identifier: 'equipment',
text: this.$t('equipment'),
});
categories.push({
identifier: 'cards',
text: this.$t('cards'),
items: _map(_filter(this.content.cardTypes, (value) => {
return value.yearRound;
}), (value) => {
return {
...getItemInfo(this.user, 'card', value),
showCount: false,
};
}),
});
let specialItems = [{
...getItemInfo(this.user, 'fortify'),
showCount: false,
}];
if (this.user.purchased.plan.customerId) {
let gemItem = getItemInfo(this.user, 'gem');
specialItems.push({
...gemItem,
showCount: false,
});
}
if (this.user.flags.rebirthEnabled) {
let rebirthItem = getItemInfo(this.user, 'rebirth_orb');
specialItems.push({
showCount: false,
...rebirthItem,
});
}
if (specialItems.length > 0) {
categories.push({
identifier: 'special',
text: this.$t('special'),
items: specialItems,
});
}
categories.map((category) => {
if (!this.viewOptions[category.identifier]) {
this.$set(this.viewOptions, category.identifier, {
selected: true,
});
}
});
return categories;
} else {
return [];
}
},
drawerTabs () {
return [
{
key: 'eggs',
contentType: 'eggs',
label: this.$t('eggs'),
},
{
key: 'food',
contentType: 'food',
label: this.$t('foodTitle'),
},
{
key: 'hatchingPotions',
contentType: 'hatchingPotions',
label: this.$t('hatchingPotions'),
},
{
key: 'special',
contentType: 'food',
label: this.$t('special'),
},
let categories = [
...this.market.categories,
];
},
gemsLeft () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap + this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
categories.push({
identifier: 'equipment',
text: this.$t('equipment'),
});
categories.push({
identifier: 'cards',
text: this.$t('cards'),
items: _map(_filter(this.content.cardTypes, (value) => {
return value.yearRound;
}), (value) => {
return {
...getItemInfo(this.user, 'card', value),
showCount: false,
};
}),
});
let specialItems = [{
...getItemInfo(this.user, 'fortify'),
showCount: false,
}];
if (this.user.purchased.plan.customerId) {
let gemItem = getItemInfo(this.user, 'gem');
specialItems.push({
...gemItem,
showCount: false,
});
}
if (this.user.flags.rebirthEnabled) {
let rebirthItem = getItemInfo(this.user, 'rebirth_orb');
specialItems.push({
showCount: false,
...rebirthItem,
});
}
if (specialItems.length > 0) {
categories.push({
identifier: 'special',
text: this.$t('special'),
items: specialItems,
});
}
categories.map((category) => {
if (!this.viewOptions[category.identifier]) {
this.$set(this.viewOptions, category.identifier, {
selected: true,
});
}
});
return categories;
},
},
methods: {
getClassName (classType) {
if (classType === 'wizard') {
return this.$t('mage');
} else {
return this.$t(classType);
}
},
tabSelected ($event) {
this.selectedDrawerTab = $event;
this.selectedDrawerItemType = this.drawerTabs[$event].key;
sellItem (itemScope) {
this.$root.$emit('sellItem', itemScope);
},
ownedItems (type) {
let mappedItems = _filter(this.content[type], i => {
@ -620,133 +333,18 @@ export default {
return this.$t('noItemsAvailableForType', { type: this.$t(`${type}ItemType`) });
}
},
getItemClass (type, itemKey) {
switch (type) {
case 'food':
case 'special':
return `Pet_Food_${itemKey}`;
case 'eggs':
return `Pet_Egg_${itemKey}`;
case 'hatchingPotions':
return `Pet_HatchingPotion_${itemKey}`;
default:
return '';
}
},
getItemName (type, item) {
switch (type) {
case 'eggs':
return this.$t('egg', {eggType: item.text()});
case 'hatchingPotions':
return this.$t('potion', {potionType: item.text()});
default:
return item.text();
}
},
filteredGear (groupByClass, searchBy, sortBy, hideLocked, hidePinned) {
let category = _filter(this.marketGearCategories, ['identifier', groupByClass]);
let result = _filter(category[0].items, (gear) => {
if (hideLocked && gear.locked) {
return false;
}
if (hidePinned && gear.pinned) {
return false;
}
if (searchBy) {
let foundPosition = gear.text.toLowerCase().indexOf(searchBy);
if (foundPosition === -1) {
return false;
}
}
// hide already owned
return !this.userItems.gear.owned[gear.key];
});
// first all unlocked
// then the selected sort
result = _sortBy(result, [(item) => item.locked, sortGearTypeMap[sortBy]]);
return result;
},
sortedMarketItems (category, sortBy, searchBy, hidePinned) {
let result = _map(category.items, (e) => {
return {
...e,
pinned: isPinned(this.user, e),
};
});
result = _filter(result, (item) => {
if (hidePinned && item.pinned) {
return false;
}
if (searchBy) {
let foundPosition = item.text.toLowerCase().indexOf(searchBy);
if (foundPosition === -1) {
return false;
}
}
return true;
});
switch (sortBy) {
case 'AZ': {
result = _sortBy(result, ['text']);
break;
}
case 'sortByNumber': {
result = _sortBy(result, item => {
if (item.showCount === false) return 0;
return this.userItems[item.purchaseType][item.key] || 0;
});
break;
}
}
return result;
},
resetItemToSell ($event) {
if (!$event) {
this.selectedItemToSell = null;
}
},
isGearLocked (gear) {
if (gear.klass !== this.userStats.class) {
return true;
}
return false;
},
togglePinned (item) {
if (!this.$store.dispatch('user:togglePinnedItem', {type: item.pinType, path: item.path})) {
this.$parent.showUnpinNotification(item);
}
},
itemSelected (item) {
this.$root.$emit('buyModal::showItem', item);
},
featuredItemSelected (item) {
if (item.purchaseType === 'gear') {
this.gearSelected(item);
if (!item.locked) {
this.itemSelected(item);
}
} else {
this.itemSelected(item);
}
},
gearSelected (item) {
if (!item.locked) {
this.$root.$emit('buyModal::showItem', item);
}
},
},
created () {
this.selectedGroupGearByClass = this.userStats.class;
},
};
</script>

Some files were not shown because too many files have changed in this diff Show more