Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc358c3aab | ||
|
|
581271e930 | ||
|
|
8537793308 | ||
|
|
38974617b5 | ||
|
|
4974712d6c | ||
|
|
832acb1617 | ||
|
|
ce1d1a763b | ||
|
|
06f522e2c9 | ||
|
|
f7ac6e8eb1 | ||
|
|
8ea1ace239 | ||
|
|
1da6eeb169 | ||
|
|
b539dd2b2e | ||
|
|
71e565ea9a | ||
|
|
565f38f7ed | ||
|
|
56ccf19c26 | ||
|
|
6a5097c16e | ||
|
|
4ff1368db7 | ||
|
|
7025f92c0e | ||
|
|
3c73322980 | ||
|
|
fbb9b2c65a | ||
|
|
a9f3360890 | ||
|
|
a0941ffa84 | ||
|
|
a9757b2d74 | ||
|
|
89fff49d02 | ||
|
|
b8b0d668c9 | ||
|
|
1d3006ae29 | ||
|
|
a63cc84779 | ||
|
|
a06974d354 | ||
|
|
f72eef6bff | ||
|
|
9e25360102 | ||
|
|
ce70c73d49 | ||
|
|
cdd87abcf9 | ||
|
|
6bea232d47 | ||
|
|
cebb3f0f25 | ||
|
|
e072d7c09c | ||
|
|
e332876d30 | ||
|
|
08d71cc0bb | ||
|
|
c4f870c421 | ||
|
|
6421f6bbe0 | ||
|
|
ac7c8e0eb6 | ||
|
|
7c9df3b32f | ||
|
|
edefae72bd | ||
|
|
132ecc8bb1 | ||
|
|
ca5b02d0ea | ||
|
|
941194b7c7 | ||
|
|
d8a7cad1a1 | ||
|
|
fa83d1a9cf | ||
|
|
d94851f759 | ||
|
|
2137c190b3 | ||
|
|
428e693711 | ||
|
|
a7aa489960 | ||
|
|
c917c7c4a9 | ||
|
|
e661838ed7 | ||
|
|
af09b6b454 | ||
|
|
f8b3891a0c | ||
|
|
631d18244b | ||
|
|
7883ba8228 | ||
|
|
605c2265c5 | ||
|
|
1bb0319012 | ||
|
|
1f0fd7d8b4 | ||
|
|
6b4b53a430 | ||
|
|
331ea18c42 | ||
|
|
db53d87cba | ||
|
|
ebebbbaac5 | ||
|
|
63376b918e | ||
|
|
ba96cd6e24 | ||
|
|
9e0e2a83be | ||
|
|
1fa926ac04 | ||
|
|
c71f0b3fda | ||
|
|
e8f5958f77 | ||
|
|
2803db73e5 | ||
|
|
0f9adf6675 | ||
|
|
63414a80fe | ||
|
|
e73e8bfb9e | ||
|
|
694fe5a273 | ||
|
|
82ebe71eb4 | ||
|
|
58d58ff962 | ||
|
|
04dcb27501 | ||
|
|
c9395ba1ca | ||
|
|
e9845e7b01 | ||
|
|
faa7ff6328 | ||
|
|
50cc7ee09a | ||
|
|
db56134832 | ||
|
|
1ea954ab10 | ||
|
|
e405372319 | ||
|
|
8f64afe9df | ||
|
|
bf0e640fa6 | ||
|
|
a6792a4f08 | ||
|
|
0007736f5c | ||
|
|
d564944507 | ||
|
|
b679cfb935 | ||
|
|
464e4f10b2 | ||
|
|
647b27c55f | ||
|
|
ac4e6490d9 | ||
|
|
a3784e98a3 | ||
|
|
5931f02692 | ||
|
|
e21aa074e4 | ||
|
|
fd038bd150 | ||
|
|
699be3a3cc | ||
|
|
b594d2bb29 |
62
migrations/archive/2023/20230808_guild_gems_fix.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable no-console */
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
import { TransactionModel as Transaction } from '../../../website/server/models/transaction';
|
||||
|
||||
const transactionsPerRun = 500;
|
||||
const progressCount = 1000;
|
||||
const transactionsQuery = {
|
||||
transactionType: 'create_guild',
|
||||
amount: { $gt: 0 },
|
||||
};
|
||||
|
||||
let count = 0;
|
||||
async function updateTransaction (transaction) {
|
||||
count++;
|
||||
if (count % progressCount === 0) {
|
||||
console.warn(`${count} ${transaction._id}`);
|
||||
}
|
||||
|
||||
const leader = await User
|
||||
.findOne({ _id: transaction.userId })
|
||||
.select({ _id: true })
|
||||
.exec();
|
||||
|
||||
if (!leader) {
|
||||
return console.warn(`User not found for transaction ${transaction._id}`);
|
||||
}
|
||||
|
||||
return leader.updateOne(
|
||||
{ $inc: { balance: transaction.amount }},
|
||||
).exec();
|
||||
}
|
||||
|
||||
export default async function processTransactions () {
|
||||
const transactionFields = {
|
||||
_id: 1,
|
||||
userId: 1,
|
||||
currency: 1,
|
||||
amount: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const foundTransactions = await Transaction // eslint-disable-line no-await-in-loop
|
||||
.find(transactionsQuery)
|
||||
.limit(transactionsPerRun)
|
||||
.sort({ _id: 1 })
|
||||
.select(transactionFields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (foundTransactions.length === 0) {
|
||||
console.warn('All appropriate transactions found and modified.');
|
||||
console.warn(`\n${count} transactions processed\n`);
|
||||
break;
|
||||
} else {
|
||||
transactionsQuery._id = {
|
||||
$gt: foundTransactions[foundTransactions.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(foundTransactions.map(txn => updateTransaction(txn))); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20230801_veteran_pet_ladder';
|
||||
const MIGRATION_NAME = '20230808_veteran_pet_ladder';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
@@ -110,7 +110,7 @@ async function updateUser (user) {
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2023-07-01') },
|
||||
// 'auth.timestamps.loggedin': { $gt: new Date('2023-07-08') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
118
migrations/archive/2023/20231017_pet_group_achievements.js
Normal file
@@ -0,0 +1,118 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20231017_pet_group_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['Armadillo-Base']
|
||||
&& pets['Armadillo-CottonCandyBlue']
|
||||
&& pets['Armadillo-CottonCandyPink']
|
||||
&& pets['Armadillo-Desert']
|
||||
&& pets['Armadillo-Golden']
|
||||
&& pets['Armadillo-Red']
|
||||
&& pets['Armadillo-Shade']
|
||||
&& pets['Armadillo-Skeleton']
|
||||
&& pets['Armadillo-White']
|
||||
&& pets['Armadillo-Zombie']
|
||||
&& pets['Cactus-Base']
|
||||
&& pets['Cactus-CottonCandyBlue']
|
||||
&& pets['Cactus-CottonCandyPink']
|
||||
&& pets['Cactus-Desert']
|
||||
&& pets['Cactus-Golden']
|
||||
&& pets['Cactus-Red']
|
||||
&& pets['Cactus-Shade']
|
||||
&& pets['Cactus-Skeleton']
|
||||
&& pets['Cactus-White']
|
||||
&& pets['Cactus-Zombie']
|
||||
&& pets['Fox-Base']
|
||||
&& pets['Fox-CottonCandyBlue']
|
||||
&& pets['Fox-CottonCandyPink']
|
||||
&& pets['Fox-Desert']
|
||||
&& pets['Fox-Golden']
|
||||
&& pets['Fox-Red']
|
||||
&& pets['Fox-Shade']
|
||||
&& pets['Fox-Skeleton']
|
||||
&& pets['Fox-White']
|
||||
&& pets['Fox-Zombie']
|
||||
&& pets['Frog-Base']
|
||||
&& pets['Frog-CottonCandyBlue']
|
||||
&& pets['Frog-CottonCandyPink']
|
||||
&& pets['Frog-Desert']
|
||||
&& pets['Frog-Golden']
|
||||
&& pets['Frog-Red']
|
||||
&& pets['Frog-Shade']
|
||||
&& pets['Frog-Skeleton']
|
||||
&& pets['Frog-White']
|
||||
&& pets['Frog-Zombie']
|
||||
&& pets['Snake-Base']
|
||||
&& pets['Snake-CottonCandyBlue']
|
||||
&& pets['Snake-CottonCandyPink']
|
||||
&& pets['Snake-Desert']
|
||||
&& pets['Snake-Golden']
|
||||
&& pets['Snake-Red']
|
||||
&& pets['Snake-Shade']
|
||||
&& pets['Snake-Skeleton']
|
||||
&& pets['Snake-White']
|
||||
&& pets['Snake-Zombie']
|
||||
&& pets['Spider-Base']
|
||||
&& pets['Spider-CottonCandyBlue']
|
||||
&& pets['Spider-CottonCandyPink']
|
||||
&& pets['Spider-Desert']
|
||||
&& pets['Spider-Golden']
|
||||
&& pets['Spider-Red']
|
||||
&& pets['Spider-Shade']
|
||||
&& pets['Spider-Skeleton']
|
||||
&& pets['Spider-White']
|
||||
&& pets['Spider-Zombie']) {
|
||||
set['achievements.duneBuddy'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.updateOne({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2023-09-16') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
2439
package-lock.json
generated
39
package.json
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.0.0",
|
||||
"version": "5.10.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.5",
|
||||
"@babel/preset-env": "^7.22.5",
|
||||
"@babel/core": "^7.22.10",
|
||||
"@babel/preset-env": "^7.22.10",
|
||||
"@babel/register": "^7.22.5",
|
||||
"@google-cloud/trace-agent": "^7.1.2",
|
||||
"@parse/node-apn": "^5.1.3",
|
||||
"@parse/node-apn": "^5.2.3",
|
||||
"@slack/webhook": "^6.1.0",
|
||||
"accepts": "^1.3.8",
|
||||
"amazon-payments": "^0.2.9",
|
||||
"amplitude": "^6.0.0",
|
||||
"apidoc": "^0.54.0",
|
||||
"apple-auth": "^1.0.9",
|
||||
"bcrypt": "^5.1.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap": "^4.6.2",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-session": "^2.0.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
@@ -31,52 +31,53 @@
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^8.1.0",
|
||||
"got": "^11.8.3",
|
||||
"got": "^11.8.6",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"helmet": "^4.6.0",
|
||||
"image-size": "^1.0.2",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"jsonwebtoken": "^9.0.1",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
"lodash": "^4.17.21",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.13.7",
|
||||
"mongoose": "^5.13.20",
|
||||
"morgan": "^1.10.0",
|
||||
"nconf": "^0.12.0",
|
||||
"node-gcm": "^1.0.5",
|
||||
"on-headers": "^1.0.2",
|
||||
"passport": "^0.5.0",
|
||||
"passport": "^0.5.3",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"paypal-rest-sdk": "^1.8.1",
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"redis": "^3.1.2",
|
||||
"regenerator-runtime": "^0.13.11",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
"stripe": "^12.9.0",
|
||||
"superagent": "^8.0.9",
|
||||
"stripe": "^12.18.0",
|
||||
"superagent": "^8.1.2",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.9.0",
|
||||
"validator": "^13.11.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.9.0",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.2.1",
|
||||
"xml2js": "^0.6.0"
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"private": true,
|
||||
"engines": {
|
||||
@@ -110,11 +111,11 @@
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.3.6",
|
||||
"axios": "^1.4.0",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^5.2.0",
|
||||
"chalk": "^5.3.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
@@ -122,7 +123,7 @@
|
||||
"monk": "^7.3.4",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon": "^15.1.2",
|
||||
"sinon": "^15.2.0",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateChallenge,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /challenges/:challengeId/flag', () => {
|
||||
let user;
|
||||
let challenge;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'TestParty',
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
user = groupLeader;
|
||||
|
||||
challenge = await generateChallenge(user, group);
|
||||
});
|
||||
|
||||
it('returns an error when challenge is not found', async () => {
|
||||
await expect(user.post(`/challenges/${generateUUID()}/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('flags a challenge', async () => {
|
||||
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
|
||||
|
||||
expect(flagResult.challenge.flags[user._id]).to.equal(true);
|
||||
expect(flagResult.challenge.flagCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('flags a challenge with a higher count when from an admin', async () => {
|
||||
await user.update({ 'contributor.admin': true });
|
||||
|
||||
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
|
||||
|
||||
expect(flagResult.challenge.flags[user._id]).to.equal(true);
|
||||
expect(flagResult.challenge.flagCount).to.equal(5);
|
||||
});
|
||||
|
||||
it('returns an error when user tries to flag a challenge that is already flagged', async () => {
|
||||
await user.post(`/challenges/${challenge._id}/flag`);
|
||||
|
||||
await expect(user.post(`/challenges/${challenge._id}/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('messageChallengeFlagAlreadyReported'),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateChallenge,
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /challenges/:challengeId/clearflags', () => {
|
||||
let admin;
|
||||
let nonAdmin;
|
||||
let challenge;
|
||||
|
||||
beforeEach(async () => {
|
||||
const { group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'TestParty',
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
|
||||
admin = groupLeader;
|
||||
[nonAdmin] = members;
|
||||
|
||||
await admin.update({ 'permissions.moderator': true });
|
||||
|
||||
challenge = await generateChallenge(admin, group);
|
||||
await admin.post(`/challenges/${challenge._id}/flag`);
|
||||
});
|
||||
|
||||
it('returns error when non-admin attempts to clear flags', async () => {
|
||||
await expect(nonAdmin.post(`/challenges/${generateUUID()}/clearflags`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('messageGroupChatAdminClearFlagCount'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when challenge is not found', async () => {
|
||||
await expect(admin.post(`/challenges/${generateUUID()}/clearflags`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('clears flags and sets mod flag to false', async () => {
|
||||
await nonAdmin.post(`/challenges/${challenge._id}/flag`);
|
||||
const flagResult = await admin.post(`/challenges/${challenge._id}/clearflags`);
|
||||
|
||||
expect(flagResult.challenge.flagCount).to.eql(0);
|
||||
expect(flagResult.challenge.flags).to.have.property(admin._id, false);
|
||||
expect(flagResult.challenge.flags).to.have.property(nonAdmin._id, true);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
const INVITES_LIMIT = 100;
|
||||
const PARTY_LIMIT_MEMBERS = 29;
|
||||
const PARTY_LIMIT_MEMBERS = 30;
|
||||
const MAX_EMAIL_INVITES_BY_USER = 200;
|
||||
|
||||
describe('Post /groups/:groupId/invite', () => {
|
||||
@@ -571,7 +571,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('partyExceedsMembersLimit', { maxMembersParty: PARTY_LIMIT_MEMBERS + 1 }),
|
||||
message: t('partyExceedsMembersLimit', { maxMembersParty: PARTY_LIMIT_MEMBERS }),
|
||||
});
|
||||
}).timeout(10000);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /members/:memberId/clear-flags', () => {
|
||||
let reporter;
|
||||
let admin;
|
||||
let moderator;
|
||||
|
||||
beforeEach(async () => {
|
||||
reporter = await generateUser();
|
||||
admin = await generateUser({ permissions: { userSupport: true } });
|
||||
moderator = await generateUser({ permissions: { moderator: true } });
|
||||
await reporter.post(`/members/${admin._id}/flag`);
|
||||
});
|
||||
|
||||
context('error cases', () => {
|
||||
it('returns error when memberId is not a UUID', async () => {
|
||||
await expect(moderator.post('/members/gribbly/clear-flags'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when member with UUID is not found', async () => {
|
||||
const randomId = generateUUID();
|
||||
|
||||
await expect(moderator.post(`/members/${randomId}/clear-flags`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', { userId: randomId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when requesting user is not a moderator', async () => {
|
||||
await expect(reporter.post(`/members/${admin._id}/clear-flags`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Only a moderator may clear reports from a profile.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('valid request', () => {
|
||||
it('removes a single flag from user', async () => {
|
||||
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
|
||||
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
|
||||
expect(updatedTarget.profile.flags).to.eql({});
|
||||
});
|
||||
|
||||
it('removes multiple flags from user', async () => {
|
||||
await moderator.post(`/members/${admin._id}/flag`);
|
||||
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
|
||||
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
|
||||
expect(updatedTarget.profile.flags).to.eql({});
|
||||
});
|
||||
});
|
||||
});
|
||||
151
test/api/v3/integration/members/POST-members_memberId_flag.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
import moment from 'moment';
|
||||
import nconf from 'nconf';
|
||||
import { IncomingWebhook } from '@slack/webhook';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /members/:memberId/flag', () => {
|
||||
let reporter;
|
||||
let target;
|
||||
|
||||
beforeEach(async () => {
|
||||
reporter = await generateUser();
|
||||
target = await generateUser({
|
||||
'profile.blurb': 'Naughty Text',
|
||||
'profile.imageUrl': 'https://evil.com/',
|
||||
});
|
||||
});
|
||||
|
||||
context('error cases', () => {
|
||||
it('returns error when memberId is not a UUID', async () => {
|
||||
await expect(reporter.post('/members/gribbly/flag'))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when member with UUID is not found', async () => {
|
||||
const randomId = generateUUID();
|
||||
|
||||
await expect(reporter.post(`/members/${randomId}/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('userWithIDNotFound', { userId: randomId }),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when non-admin flags same profile twice', async () => {
|
||||
await reporter.post(`/members/${target._id}/flag`);
|
||||
await expect(reporter.post(`/members/${target._id}/flag`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'A profile can not be flagged more than once by the same user.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('valid request', () => {
|
||||
let admin;
|
||||
const comment = 'this profile is bad';
|
||||
const source = 'Third Party Script';
|
||||
|
||||
beforeEach(async () => {
|
||||
admin = await generateUser({ 'permissions.userSupport': true });
|
||||
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
it('adds flags object to target user', async () => {
|
||||
await reporter.post(`/members/${target._id}/flag`);
|
||||
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
|
||||
expect(updatedTarget.profile.flags[reporter._id]).to.have.all.keys([
|
||||
'comment',
|
||||
'source',
|
||||
'timestamp',
|
||||
]);
|
||||
expect(moment(updatedTarget.profile.flags[reporter._id].timestamp).toDate()).to.be.a('date');
|
||||
});
|
||||
|
||||
it('allows addition of a comment and source', async () => {
|
||||
await reporter.post(`/members/${target._id}/flag`, {
|
||||
comment,
|
||||
source,
|
||||
});
|
||||
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
|
||||
expect(updatedTarget.profile.flags[reporter._id].comment).to.eql(comment);
|
||||
expect(updatedTarget.profile.flags[reporter._id].source).to.eql(source);
|
||||
});
|
||||
|
||||
it('allows moderator to flag twice', async () => {
|
||||
const moderator = await generateUser({ 'permissions.moderator': true });
|
||||
await moderator.post(`/members/${target._id}/flag`);
|
||||
await expect(moderator.post(`/members/${target._id}/flag`)).to.eventually.be.ok;
|
||||
});
|
||||
|
||||
it('allows multiple non-moderators to flag individually', async () => {
|
||||
await admin.post(`/members/${target._id}/flag`);
|
||||
await reporter.post(`/members/${target._id}/flag`);
|
||||
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
|
||||
expect(updatedTarget.profile.flags[admin._id]).to.exist;
|
||||
expect(updatedTarget.profile.flags[reporter._id]).to.exist;
|
||||
});
|
||||
|
||||
it('sends a flag report to moderation Slack', async () => {
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
await reporter.post(`/members/${target._id}/flag`, {
|
||||
comment,
|
||||
source,
|
||||
});
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${target.auth.local.username}'s profile from ${source} and commented: ${comment}`,
|
||||
attachments: [{
|
||||
fallback: 'Flag Profile',
|
||||
color: 'danger',
|
||||
title: 'User Profile Report',
|
||||
title_link: `${BASE_URL}/profile/${target._id}`,
|
||||
text: `Display Name: ${target.profile.name}\n\nImage URL: ${target.profile.imageUrl}\n\nAbout: ${target.profile.blurb}`,
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
}],
|
||||
});
|
||||
/* eslint-enable camelcase */
|
||||
});
|
||||
|
||||
it('excludes empty fields when sending Slack message', async () => {
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
await reporter.post(`/members/${admin._id}/flag`, {
|
||||
comment,
|
||||
source,
|
||||
});
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${admin.auth.local.username}'s profile from ${source} and commented: ${comment}`,
|
||||
attachments: [{
|
||||
fallback: 'Flag Profile',
|
||||
color: 'danger',
|
||||
title: 'User Profile Report',
|
||||
title_link: `${BASE_URL}/profile/${admin._id}`,
|
||||
text: `Display Name: ${admin.profile.name}`,
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
}],
|
||||
});
|
||||
/* eslint-enable camelcase */
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,9 @@ describe('POST /user/reset', () => {
|
||||
type: 'habit',
|
||||
});
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.post('/user/reset', {
|
||||
password: 'password',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||
@@ -39,7 +41,9 @@ describe('POST /user/reset', () => {
|
||||
type: 'daily',
|
||||
});
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.post('/user/reset', {
|
||||
password: 'password',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||
@@ -57,7 +61,9 @@ describe('POST /user/reset', () => {
|
||||
type: 'todo',
|
||||
});
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.post('/user/reset', {
|
||||
password: 'password',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||
@@ -75,7 +81,9 @@ describe('POST /user/reset', () => {
|
||||
type: 'reward',
|
||||
});
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.post('/user/reset', {
|
||||
password: 'password',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
|
||||
@@ -87,6 +95,26 @@ describe('POST /user/reset', () => {
|
||||
expect(user.tasksOrder.rewards).to.be.empty;
|
||||
});
|
||||
|
||||
it('does not allow to reset if the password is missing', async () => {
|
||||
await expect(user.post('/user/reset', {
|
||||
password: '',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('missingPassword'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not allow to reset if the password is wrong', async () => {
|
||||
await expect(user.post('/user/reset', {
|
||||
password: 'passdw',
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('wrongPassword'),
|
||||
});
|
||||
});
|
||||
|
||||
it('does not delete challenge or group tasks', async () => {
|
||||
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
|
||||
const challenge = await generateChallenge(user, guild);
|
||||
@@ -102,7 +130,9 @@ describe('POST /user/reset', () => {
|
||||
});
|
||||
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
|
||||
|
||||
await user.post('/user/reset');
|
||||
await user.post('/user/reset', {
|
||||
password: 'password',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
await user.put('/user', {
|
||||
@@ -133,7 +163,9 @@ describe('POST /user/reset', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await hero.post('/user/reset');
|
||||
await user.post('/user/reset', {
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
const heroRes = await admin.get(`/hall/heroes/${hero.auth.local.username}`);
|
||||
|
||||
|
||||
@@ -126,13 +126,5 @@ describe('shared.ops.addTask', () => {
|
||||
expect(addTask(user)._editing).not.be.ok;
|
||||
expect(addTask(user)._edit).to.not.be.ok;
|
||||
});
|
||||
|
||||
it('respects advancedCollapsed preference', () => {
|
||||
user.preferences.advancedCollapsed = true;
|
||||
expect(addTask(user)._advanced).not.be.ok;
|
||||
|
||||
user.preferences.advancedCollapsed = false;
|
||||
expect(addTask(user)._advanced).to.be.ok;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,14 +197,14 @@ describe('shared.ops.purchase', () => {
|
||||
|
||||
it('purchases quest bundles', async () => {
|
||||
const startingBalance = user.balance;
|
||||
const clock = sandbox.useFakeTimers(moment('2019-05-20').valueOf());
|
||||
const clock = sandbox.useFakeTimers(moment('2022-03-16').valueOf());
|
||||
const type = 'bundles';
|
||||
const key = 'featheredFriends';
|
||||
const key = 'cuddleBuddies';
|
||||
const price = 1.75;
|
||||
const questList = [
|
||||
'falcon',
|
||||
'harpy',
|
||||
'owl',
|
||||
'bunny',
|
||||
'ferret',
|
||||
'guineapig',
|
||||
];
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
|
||||
@@ -15,6 +15,7 @@ module.exports = {
|
||||
'import/no-unresolved': 'off',
|
||||
'import/extensions': 'off',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-mutating-props': 'warn',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'never',
|
||||
|
||||
@@ -55,3 +55,13 @@ in a separate `.add('function of component', ...`
|
||||
### Storybook Build
|
||||
|
||||
After each client build, storybook build is also triggered and will be available in `dist/storybook`
|
||||
|
||||
### Vue Structure
|
||||
|
||||
Currently pages and components are mixed in `/src/components` this is not a good way to find the files easy.
|
||||
|
||||
Thats why each changed / upcoming page / component should be put in either `/src/components` or in the `/src/pages` directory.
|
||||
|
||||
Inside Pages, each page can have a subfolder which contains sub-components only needed for that page - otherwise it has to be added to the normal components folder.
|
||||
|
||||
At the end of all the changes - the components should only contain components needed between all pages
|
||||
|
||||
817
website/client/package-lock.json
generated
@@ -32,23 +32,23 @@
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"chai": "^4.3.7",
|
||||
"core-js": "^3.31.0",
|
||||
"core-js": "^3.32.2",
|
||||
"dompurify": "^3.0.3",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"eslint-plugin-vue": "^7.20.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.20.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"intro.js": "^7.0.1",
|
||||
"intro.js": "^7.2.0",
|
||||
"jquery": "^3.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nconf": "^0.12.0",
|
||||
"sass": "^1.63.4",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.19.2",
|
||||
"smartbanner.js": "^1.19.3",
|
||||
"stopword": "^2.0.8",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"svg-url-loader": "^7.1.1",
|
||||
@@ -58,12 +58,14 @@
|
||||
"validator": "^13.9.0",
|
||||
"vue": "^2.7.10",
|
||||
"vue-cli-plugin-storybook": "2.1.0",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-template-compiler": "^2.7.10",
|
||||
"vue-template-babel-compiler": "^2.0.0",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^4.46.0"
|
||||
"webpack": "^4.47.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.219rem 0.75rem;
|
||||
border-radius: 2px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
|
||||
color: $white;
|
||||
|
||||
&:hover, &:focus {
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
|
||||
&:disabled, &.disabled, &.btn-flat {
|
||||
&.btn-flat {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
@@ -264,6 +264,10 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
.dropdown-toggle:hover {
|
||||
--caret-color: #{$purple-300};
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown.show > .dropdown-toggle:not(.btn-success) {
|
||||
@@ -136,6 +140,8 @@
|
||||
|
||||
.dropdown-menu.show {
|
||||
min-width: 100% !important;
|
||||
overflow: scroll;
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,11 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
color: $gray-50;
|
||||
border: 1px solid $gray-400;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
&:hover:not(:disabled):not(:read-only) {
|
||||
border-color: $gray-300;
|
||||
}
|
||||
|
||||
&:active:not(:disabled), &:focus:not(:disabled) {
|
||||
&:active:not(:disabled):not(:read-only), &:focus:not(:disabled):not(:read-only) {
|
||||
border-color: $purple-400;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
@@ -56,13 +56,13 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
|
||||
&.input-valid, &.input-invalid {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right 16px;
|
||||
background-position: center right 0.5rem;
|
||||
}
|
||||
|
||||
&.input-valid {
|
||||
padding-right: 37px;
|
||||
padding-right: 27px;
|
||||
background-image: url(~@/assets/svg/for-css/check.svg);
|
||||
background-size: 13px 10px;
|
||||
background-size: 1rem;
|
||||
}
|
||||
|
||||
&.input-invalid {
|
||||
@@ -91,8 +91,10 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
border-color: $gray-300;
|
||||
}
|
||||
|
||||
&:focus, &:active, &:focus-within {
|
||||
border: solid 1px $purple-400;
|
||||
&:not(:read-only) {
|
||||
&:focus, &:active, &:focus-within {
|
||||
border: solid 1px $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group-prepend , .input-group-append {
|
||||
@@ -163,8 +165,22 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
input {
|
||||
height: 30px;
|
||||
border: 0;
|
||||
background: $white !important;
|
||||
}
|
||||
|
||||
&.is-valid {
|
||||
border-color: $green-10 !important;
|
||||
}
|
||||
|
||||
&.is-invalid {
|
||||
border-color: $red-100 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.input-error {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
|
||||
color: $maroon-10;
|
||||
}
|
||||
|
||||
.input-group-spaced {
|
||||
@@ -231,20 +247,20 @@ $bg-disabled-control: $gray-10;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
|
||||
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
|
||||
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
|
||||
border: 2px solid $gray-300;
|
||||
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
|
||||
}
|
||||
|
||||
&:focus:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:focus:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:active:checked:not(:disabled)~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
|
||||
border-color: 2 px solid $purple-400;
|
||||
background-color: $purple-400;
|
||||
}
|
||||
|
||||
&:focus:disabled~.custom-control-label::before,
|
||||
&:focus:disabled~.custom-control-label::before,
|
||||
&:active:disabled~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
|
||||
}
|
||||
@@ -398,8 +414,6 @@ $bg-color: $purple-400;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Disable default style Firefox for invalid elements.
|
||||
// Selectors taken from view-source:resource://gre-resources/forms.css on Firefox
|
||||
:not(output):-moz-ui-invalid {
|
||||
|
||||
@@ -1,55 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="1000"
|
||||
viewBox="0 0 1000 1187.198"
|
||||
version="1.1"
|
||||
height="1187.198"
|
||||
id="svg2"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="Apple_1998.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="705"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.1767767"
|
||||
inkscape:cx="-1066.5045"
|
||||
inkscape:cy="964.94669"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="m 979.04184,925.18785 c -17.95397,41.47737 -39.20563,79.65705 -63.82824,114.75895 -33.56298,47.8528 -61.04356,80.9761 -82.22194,99.3698 -32.83013,30.192 -68.00529,45.6544 -105.67203,46.5338 -27.04089,0 -59.6512,-7.6946 -97.61105,-23.3035 -38.08442,-15.5358 -73.08371,-23.2303 -105.08578,-23.2303 -33.56296,0 -69.55888,7.6945 -108.06101,23.2303 -38.5608,15.6089 -69.62484,23.7432 -93.37541,24.5493 -36.12049,1.5389 -72.1237,-14.3632 -108.06101,-47.7796 -22.93711,-20.0059 -51.62684,-54.3017 -85.99592,-102.8874 C 92.254176,984.54592 61.937588,924.38175 38.187028,855.7902 12.750995,781.70252 0,709.95986 0,640.50361 0,560.94181 17.191859,492.32094 51.626869,434.81688 78.689754,388.62753 114.69299,352.19192 159.75381,325.44413 c 45.06086,-26.74775 93.74914,-40.37812 146.18212,-41.25019 28.68971,0 66.3125,8.8744 113.06613,26.31542 46.62174,17.49964 76.55727,26.37404 89.68198,26.37404 9.8124,0 43.06758,-10.37669 99.4431,-31.06405 53.31237,-19.18512 98.30724,-27.12887 135.16787,-23.99975 99.8828,8.06098 174.92313,47.43518 224.82789,118.37174 -89.33023,54.12578 -133.51903,129.93556 -132.63966,227.18753 0.8061,75.75115 28.28668,138.78795 82.2952,188.8393 24.47603,23.23022 51.81008,41.18421 82.22186,53.93522 -6.59525,19.12648 -13.557,37.44688 -20.95846,55.03446 z M 749.96366,23.751237 c 0,59.37343 -21.69138,114.810233 -64.92748,166.121963 -52.17652,60.99961 -115.28658,96.24803 -183.72426,90.68597 -0.87204,-7.12298 -1.37769,-14.61967 -1.37769,-22.49743 0,-56.99843 24.81315,-117.99801 68.87738,-167.873453 21.99909,-25.25281 49.978,-46.25018 83.90738,-63.00018 C 686.57507,10.688027 718.59913,1.5631274 748.71783,5.2734376e-4 749.59727,7.9378274 749.96366,15.875627 749.96366,23.750467 Z"
|
||||
id="path4"
|
||||
inkscape:connector-curvature="0" />
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.841 2.564c-.567.672-1.474 1.202-2.382 1.126-.113-.908.331-1.873.851-2.47C7.877.53 8.87.039 9.673 0c.095.946-.274 1.873-.832 2.564zm.823 1.306c-1.314-.076-2.439.747-3.063.747-.633 0-1.588-.71-2.627-.69-1.352.018-2.609.785-3.299 2.005-1.418 2.441-.369 6.055 1.002 8.042.67.984 1.474 2.063 2.533 2.025 1.002-.038 1.399-.653 2.609-.653 1.219 0 1.569.653 2.627.634 1.097-.019 1.787-.984 2.458-1.968.765-1.116 1.077-2.204 1.096-2.261-.019-.019-2.117-.823-2.136-3.245-.019-2.025 1.654-2.99 1.73-3.047-.946-1.4-2.42-1.551-2.93-1.59z" fill="#1A181D" fill-rule="nonzero"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 670 B |
10
website/client/src/assets/svg/crown.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#nw2v1izcda)">
|
||||
<path d="m8 4.01 2.2 5.4 3.8-3.4-2 6H4l-2-6 3.8 3.4L8 4.01zm0-2c-.81 0-1.55.49-1.85 1.25L5.02 6.03 3.33 4.52A1.98 1.98 0 0 0 2 4.01 2.002 2.002 0 0 0 .1 6.64l2 6A2 2 0 0 0 4 14.01h8a2 2 0 0 0 1.9-1.37l1.97-5.9a1.997 1.997 0 0 0-1.85-2.73h-.04c-.5.01-.95.2-1.3.51L11 6.02 9.87 3.25A2.012 2.012 0 0 0 8.02 2L8 2.01z" fill="#4E4A57"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="nw2v1izcda">
|
||||
<path fill="#fff" d="M0 0h16v16H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
@@ -1,3 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
|
||||
<path fill="#24CC8F" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<defs>
|
||||
<path id="vm46q29nca" d="M6.662 12.832c-.312 0-.61-.123-.831-.344L2 8.657l1.662-1.662 2.934 2.934L12.534 3l1.785 1.529-6.764 7.893c-.214.248-.521.396-.848.409l-.045.001"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<g transform="translate(-306 -8) translate(306 8)">
|
||||
<mask id="c8uzbxs4ob" fill="#fff">
|
||||
<use xlink:href="#vm46q29nca"/>
|
||||
</mask>
|
||||
<use fill="#878190" xlink:href="#vm46q29nca"/>
|
||||
<g fill="#20B780" mask="url(#c8uzbxs4ob)">
|
||||
<path d="M0 0H16V16H0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 284 B After Width: | Height: | Size: 808 B |
@@ -1 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="17"><defs><path id="a" d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z"/></defs><g transform="rotate(-90 8 8)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#BDA8FF" xlink:href="#a"/><g fill="#878190" mask="url(#b)"><path d="M0 0h16v16H0z"/></g></g></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" >
|
||||
<path d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z" id="myc95n2o6a"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 183 B |
5
website/client/src/assets/svg/lock-small.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd" opacity=".75" transform="translate(3 2)">
|
||||
<path fill="#878190" d="M4 9h2V7H4v2zm4 1H2V6h6v4zM5 2c1.103 0 2 .897 2 2H3c0-1.103.897-2 2-2zm4 2.277V4c0-2.209-1.791-4-4-4S1 1.791 1 4v.277C.405 4.624 0 5.262 0 6v4c0 1.105.895 2 2 2h6c1.105 0 2-.895 2-2V6c0-.738-.405-1.376-1-1.723z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
10
website/client/src/assets/svg/mute.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#464rik5fna)">
|
||||
<path d="M12 2c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2l-4 2v-2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8zm0-2H4C1.79 0 0 1.79 0 4v6c0 2.21 1.79 4 4 4h2c0 .69.36 1.34.95 1.7a1.993 1.993 0 0 0 1.94.09l3.65-1.83A4 4 0 0 0 15.99 10V4c0-2.21-1.79-4-4-4H12zM9.41 7l1.29-1.29A.996.996 0 1 0 9.29 4.3L8 5.59 6.71 4.3A.996.996 0 1 0 5.3 5.71L6.59 7 5.3 8.29a.996.996 0 0 0 .71 1.7c.26 0 .51-.1.71-.29l1.29-1.29L9.3 9.7c.2.2.45.29.71.29.26 0 .51-.1.71-.29a.996.996 0 0 0 0-1.41L9.43 7h-.02z" fill="#4E4A57"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="464rik5fna">
|
||||
<path fill="#fff" d="M0 0h16v16H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 770 B |
10
website/client/src/assets/svg/shadow-mute.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#mzh0y8jf2a)">
|
||||
<path d="M12 2c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2l-4 2v-2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8zm0-2H4C1.79 0 0 1.79 0 4v6c0 2.21 1.79 4 4 4h2c0 .69.36 1.34.95 1.7a1.993 1.993 0 0 0 1.94.09l3.65-1.83A4 4 0 0 0 15.99 10V4c0-2.21-1.79-4-4-4H12zm-.17 7c0-.55-.45-1-1-1H5.17c-.55 0-1 .45-1 1s.45 1 1 1h5.66c.55 0 1-.45 1-1z" fill="#4E4A57"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="mzh0y8jf2a">
|
||||
<path fill="#fff" d="M0 0h16v16H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 618 B |
@@ -91,6 +91,7 @@ export default {
|
||||
this.close();
|
||||
},
|
||||
hide () {
|
||||
this.$store.dispatch('user:statSync');
|
||||
this.$store.dispatch('user:set', { 'party.quest.completed': '' });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -731,6 +731,8 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
|
||||
|
||||
hello.init({
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="member.preferences"
|
||||
class="avatar"
|
||||
:style="{width, height, paddingTop}"
|
||||
:class="backgroundClass"
|
||||
@@ -184,9 +185,11 @@ export default {
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
hasClass () {
|
||||
if (!this.member) return false;
|
||||
return this.$store.getters['members:hasClass'](this.member);
|
||||
},
|
||||
isBuffed () {
|
||||
if (!this.member) return false;
|
||||
return this.$store.getters['members:isBuffed'](this.member);
|
||||
},
|
||||
paddingTop () {
|
||||
@@ -197,28 +200,30 @@ export default {
|
||||
let val = '24px';
|
||||
|
||||
if (!this.avatarOnly) {
|
||||
if (this.member.items.currentPet) val = '24px';
|
||||
if (this.member.items.currentMount) val = '0px';
|
||||
if (this.member?.items.currentPet) val = '24px';
|
||||
if (this.member?.items.currentMount) val = '0px';
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
backgroundClass () {
|
||||
const { background } = this.member.preferences;
|
||||
if (this.member) {
|
||||
const { background } = this.member.preferences;
|
||||
|
||||
const allowToShowBackground = !this.avatarOnly || this.withBackground;
|
||||
const allowToShowBackground = !this.avatarOnly || this.withBackground;
|
||||
|
||||
if (this.overrideAvatarGear && this.overrideAvatarGear.background) {
|
||||
return `background_${this.overrideAvatarGear.background}`;
|
||||
if (this.overrideAvatarGear && this.overrideAvatarGear.background) {
|
||||
return `background_${this.overrideAvatarGear.background}`;
|
||||
}
|
||||
|
||||
if (background && allowToShowBackground) {
|
||||
return `background_${this.member.preferences.background}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (background && allowToShowBackground) {
|
||||
return `background_${this.member.preferences.background}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
visualBuffs () {
|
||||
if (!this.member) return {};
|
||||
return {
|
||||
snowball: `avatar_snowball_${this.member.stats.class}`,
|
||||
spookySparkles: 'ghost',
|
||||
@@ -227,15 +232,16 @@ export default {
|
||||
};
|
||||
},
|
||||
skinClass () {
|
||||
if (!this.member) return '';
|
||||
const baseClass = `skin_${this.member.preferences.skin}`;
|
||||
|
||||
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
|
||||
},
|
||||
costumeClass () {
|
||||
return this.member.preferences.costume ? 'costume' : 'equipped';
|
||||
return this.member?.preferences.costume ? 'costume' : 'equipped';
|
||||
},
|
||||
specialMountClass () {
|
||||
if (!this.avatarOnly && this.member.items.currentMount && this.member.items.currentMount.includes('Kangaroo')) {
|
||||
if (!this.avatarOnly && this.member?.items.currentMount && this.member?.items.currentMount.includes('Kangaroo')) {
|
||||
return 'offset-kangaroo';
|
||||
}
|
||||
|
||||
@@ -248,12 +254,13 @@ export default {
|
||||
)) {
|
||||
return this.foolPet(this.member.items.currentPet);
|
||||
}
|
||||
if (this.member.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getGearClass (gearType) {
|
||||
if (!this.member) return '';
|
||||
let result = this.member.items.gear[this.costumeClass][gearType];
|
||||
|
||||
if (this.overrideAvatarGear && this.overrideAvatarGear[gearType]) {
|
||||
@@ -263,6 +270,7 @@ export default {
|
||||
return result;
|
||||
},
|
||||
hideGear (gearType) {
|
||||
if (!this.member) return true;
|
||||
if (gearType === 'weapon') {
|
||||
const equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
|
||||
|
||||
@@ -288,6 +296,7 @@ export default {
|
||||
this.$root.$emit('castEnd', this.member, 'user', e);
|
||||
},
|
||||
showAvatar () {
|
||||
if (!this.member) return false;
|
||||
if (!this.showVisualBuffs) return true;
|
||||
|
||||
const { buffs } = this.member.stats;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<report-challenge-modal />
|
||||
<challenge-modal @updatedChallenge="updatedChallenge" />
|
||||
<leave-challenge-modal
|
||||
:challenge-id="challenge._id"
|
||||
@@ -9,11 +10,27 @@
|
||||
:members="members"
|
||||
:challenge-id="challenge._id"
|
||||
:prize="challenge.prize"
|
||||
:flag-count="challenge.flagCount"
|
||||
/>
|
||||
<challenge-member-progress-modal :challenge-id="challenge._id" />
|
||||
<div class="col-12 col-md-8 standard-page">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div
|
||||
v-if="canViewFlags"
|
||||
class="flagged"
|
||||
>
|
||||
<div
|
||||
v-if="flaggedNotHidden"
|
||||
>
|
||||
{{ $t("flaggedNotHidden") }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="flaggedAndHidden"
|
||||
>
|
||||
{{ $t("flaggedAndHidden") }}
|
||||
</div>
|
||||
</div>
|
||||
<h1 v-markdown="challenge.name"></h1>
|
||||
<div>
|
||||
<span class="mr-1 ml-0 d-block">
|
||||
@@ -41,7 +58,7 @@
|
||||
createdBy string (helps with RTL languages)-->
|
||||
<!-- @TODO: Implement in V2 strong.margin-left
|
||||
(v-once).svg-icon.calendar-icon(v-html="icons.calendarIcon")
|
||||
| {{$t('endDate')}}
|
||||
{{$t('endDate')}}
|
||||
// "endDate": "End Date: <% endDate %>",-->
|
||||
<!-- span {{challenge.endDate}}-->
|
||||
</div>
|
||||
@@ -169,13 +186,16 @@
|
||||
v-if="isLeader || isAdmin"
|
||||
class="button-container"
|
||||
>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="cloneChallenge()"
|
||||
>
|
||||
{{ $t('clone') }}
|
||||
</button>
|
||||
<div>
|
||||
<button
|
||||
class="btn"
|
||||
:disabled="flaggedAndHidden"
|
||||
:class="flaggedAndHidden ? 'disabled btn-disabled' : 'btn-primary'"
|
||||
@click="cloneChallenge()"
|
||||
>
|
||||
{{ $t('clone') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isLeader || isAdmin"
|
||||
@@ -201,6 +221,17 @@
|
||||
{{ $t('endChallenge') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="button-container"
|
||||
>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-danger"
|
||||
@click="reportChallenge()"
|
||||
>
|
||||
{{ $t('report') }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<sidebar-section :title="$t('challengeSummary')">
|
||||
<p v-markdown="challenge.summary"></p>
|
||||
@@ -249,6 +280,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
background-color: $gray-700;
|
||||
color: $gray-50;
|
||||
box-shadow: none;
|
||||
cursor: arrow;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-icon {
|
||||
width: 12px;
|
||||
display: inline-block;
|
||||
@@ -312,6 +354,15 @@
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.flagged {
|
||||
margin-left: 0em;
|
||||
color: $red-10;
|
||||
|
||||
span {
|
||||
margin-left: 0em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -332,6 +383,7 @@ import challengeModal from './challengeModal';
|
||||
import challengeMemberProgressModal from './challengeMemberProgressModal';
|
||||
import challengeMemberSearchMixin from '@/mixins/challengeMemberSearch';
|
||||
import leaveChallengeModal from './leaveChallengeModal';
|
||||
import reportChallengeModal from './reportChallengeModal';
|
||||
import sidebarSection from '../sidebarSection';
|
||||
import userLink from '../userLink';
|
||||
import groupLink from '../groupLink';
|
||||
@@ -350,6 +402,7 @@ export default {
|
||||
components: {
|
||||
closeChallengeModal,
|
||||
leaveChallengeModal,
|
||||
reportChallengeModal,
|
||||
challengeModal,
|
||||
challengeMemberProgressModal,
|
||||
memberSearchDropdown,
|
||||
@@ -401,6 +454,20 @@ export default {
|
||||
canJoin () {
|
||||
return !this.isMember;
|
||||
},
|
||||
// canViewFlags should allow only moderators/admins to see flags
|
||||
canViewFlags () {
|
||||
const isModerator = this.hasPermission(this.user, 'moderator');
|
||||
if (isModerator && this.challenge.flagCount > 0) return true;
|
||||
return false;
|
||||
},
|
||||
// flaggedNotHidden should allow mods/admins & challenge owner to see flags
|
||||
flaggedNotHidden () {
|
||||
return this.challenge.flagCount === 1;
|
||||
},
|
||||
// flaggedAndHidden should only allow admin to see challenge & flags
|
||||
flaggedAndHidden () {
|
||||
return this.challenge.flagCount > 1;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'challenge.name': {
|
||||
@@ -589,6 +656,14 @@ export default {
|
||||
challenge: this.challenge,
|
||||
});
|
||||
},
|
||||
reportChallenge () {
|
||||
this.$root.$emit('habitica::report-challenge', {
|
||||
challenge: this.challenge,
|
||||
});
|
||||
},
|
||||
async showCannotCloneModal () {
|
||||
this.$root.$emit('bv::show::modal', 'cannot-clone-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -366,7 +366,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -418,6 +418,9 @@ export default {
|
||||
methods: {
|
||||
async shown () {
|
||||
this.groups = await this.$store.dispatch('guilds:getMyGuilds');
|
||||
this.groups = this.groups.filter(group => !(
|
||||
group.leaderOnly.challenges && group.leader !== this.user._id
|
||||
));
|
||||
|
||||
if (this.user.party && this.user.party._id) {
|
||||
await this.$store.dispatch('party:getParty');
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<b-modal
|
||||
id="close-challenge-modal"
|
||||
:title="$t('createGuild')"
|
||||
:title="$t('endChallenge')"
|
||||
size="md"
|
||||
>
|
||||
<div
|
||||
@@ -17,31 +17,42 @@
|
||||
</h2>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div class="col-12">
|
||||
<div class="support-habitica">
|
||||
<!-- @TODO: Add challenge achievement badge here-->
|
||||
<span
|
||||
v-if="isFlagged"
|
||||
class="col-12"
|
||||
>
|
||||
<div>{{ $t('cannotClose') }}</div>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="col-12"
|
||||
>
|
||||
<div class="col-12">
|
||||
<div class="support-habitica">
|
||||
<!-- @TODO: Add challenge achievement badge here-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<member-search-dropdown
|
||||
:text="winnerText"
|
||||
:members="members"
|
||||
:challenge-id="challengeId"
|
||||
@member-selected="selectMember"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="closeChallenge"
|
||||
>
|
||||
{{ $t('awardWinners') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<strong v-once>{{ $t('selectChallengeWinnersDescription') }}</strong>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<member-search-dropdown
|
||||
:text="winnerText"
|
||||
:members="members"
|
||||
:challenge-id="challengeId"
|
||||
@member-selected="selectMember"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="closeChallenge"
|
||||
>
|
||||
{{ $t('awardWinners') }}
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
<div class="col-12">
|
||||
<hr>
|
||||
<div class="or">
|
||||
@@ -123,7 +134,7 @@ export default {
|
||||
components: {
|
||||
memberSearchDropdown,
|
||||
},
|
||||
props: ['challengeId', 'members', 'prize'],
|
||||
props: ['challengeId', 'members', 'prize', 'flagCount'],
|
||||
data () {
|
||||
return {
|
||||
winner: {},
|
||||
@@ -134,6 +145,9 @@ export default {
|
||||
if (!this.winner.profile) return this.$t('selectMember');
|
||||
return this.winner.profile.name;
|
||||
},
|
||||
isFlagged () {
|
||||
return this.flagCount > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectMember (member) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<challenge-modal
|
||||
:group-id="groupId"
|
||||
:group-id="group._id"
|
||||
@createChallenge="challengeCreated"
|
||||
/>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="report-challenge"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div class="heading">
|
||||
<h5
|
||||
v-html="$t('abuseFlagModalHeading')"
|
||||
>
|
||||
</h5>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="svg-icon close-icon icon-16 color"
|
||||
aria-hidden="true"
|
||||
tabindex="0"
|
||||
@click="close()"
|
||||
@keypress.enter="close()"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
</div>
|
||||
<blockquote>
|
||||
<div
|
||||
v-html="abuseObject.name"
|
||||
>
|
||||
</div>
|
||||
</blockquote>
|
||||
<div>
|
||||
<span class="why-report">{{ $t('whyReportingChallenge') }}</span>
|
||||
<textarea
|
||||
v-model="reportComment"
|
||||
class="form-control"
|
||||
:placeholder="$t('whyReportingChallengePlaceholder')"
|
||||
></textarea>
|
||||
</div>
|
||||
<p
|
||||
class="report-guidelines"
|
||||
v-html="$t('abuseFlagModalBodyChallenge', abuseFlagModalBody)"
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="buttons text-center">
|
||||
<div class="button-spacing">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
:class="{disabled: !reportComment}"
|
||||
@click="reportAbuse()"
|
||||
>
|
||||
{{ $t('report') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="button-spacing">
|
||||
<a
|
||||
class="cancel-link"
|
||||
@click.prevent="close()"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasPermission(user, 'moderator')"
|
||||
class="reset-flag-count d-flex justify-content-center align-items-middle"
|
||||
@click="clearFlagCount()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon icon-16 color ml-auto mr-2 my-auto"
|
||||
v-html="icons.report"
|
||||
></div>
|
||||
<div
|
||||
class="mr-auto my-auto"
|
||||
@click="clearFlagCount()"
|
||||
>
|
||||
{{ $t('resetFlags') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#report-challenge {
|
||||
h5 {
|
||||
color: #F23035;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border: none;
|
||||
padding-bottom: 0px;
|
||||
padding-top: 12px;
|
||||
height: 16px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.modal-body {
|
||||
padding: 0px 8px 0px 8px;
|
||||
}
|
||||
|
||||
span.svg-icon.close-icon.icon-16 {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-top: -32px;
|
||||
margin-right: -16px;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
color: $gray-300;
|
||||
stroke-width: 0px;
|
||||
|
||||
&:hover {
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
.heading h5 {
|
||||
margin-bottom: 24px;
|
||||
margin-top: 16px;
|
||||
color: $red-10;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.why-report {
|
||||
font-size: 1em;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.report-guidelines {
|
||||
line-height: 1.71;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-radius: 4px;
|
||||
background-color: $gray-700;
|
||||
padding: 8px 16px 8px 16px;
|
||||
margin-top: 24px;
|
||||
font-weight: bold;
|
||||
color: $gray-10;
|
||||
height: max-conent;;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
border: solid 1px $gray-400;
|
||||
height: 64px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
padding: 0 16px 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.button-spacing {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.btn-danger.disabled {
|
||||
background-color: $white;
|
||||
color: $gray-50;
|
||||
line-height: 1.71;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
a.cancel-link {
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.reset-flag-count {
|
||||
margin: 16px -16px -16px -16px;
|
||||
height: 48px;
|
||||
color: $maroon-50;
|
||||
background-color: rgba(255, 182, 184, 0.25);
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
import svgReport from '@/assets/svg/report.svg';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [notifications, userStateMixin],
|
||||
data () {
|
||||
const abuseFlagModalBody = {
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank">',
|
||||
linkEnd: '</a>',
|
||||
};
|
||||
|
||||
return {
|
||||
abuseFlagModalBody,
|
||||
abuseObject: '',
|
||||
groupId: '',
|
||||
reportComment: '',
|
||||
icons: Object.freeze({
|
||||
close: svgClose,
|
||||
report: svgReport,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('habitica::report-challenge', this.handleReport);
|
||||
},
|
||||
destroyed () {
|
||||
this.$root.$off('habitica::report-challenge', this.handleReport);
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'report-challenge');
|
||||
},
|
||||
async reportAbuse () {
|
||||
if (!this.reportComment) return;
|
||||
this.$store.dispatch('challenges:flag', {
|
||||
challengeId: this.abuseObject.id,
|
||||
comment: this.reportComment,
|
||||
}).then(() => {
|
||||
this.text(this.$t('abuseReported'));
|
||||
this.close();
|
||||
}).catch(() => {});
|
||||
},
|
||||
async clearFlagCount () {
|
||||
await this.$store.dispatch('challenges:clearFlagCount', {
|
||||
challengeId: this.abuseObject.id,
|
||||
});
|
||||
this.close();
|
||||
},
|
||||
handleReport (data) {
|
||||
if (!data.challenge) return;
|
||||
this.abuseObject = data.challenge;
|
||||
this.reportComment = '';
|
||||
this.$root.$emit('bv::show::modal', 'report-challenge');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -39,7 +39,7 @@
|
||||
v-if="user._id !== msg.uuid && msg.uuid !== 'system'"
|
||||
class="avatar-left"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
@@ -58,7 +58,7 @@
|
||||
<avatar
|
||||
v-if="user._id === msg.uuid"
|
||||
:class="{ invisible: avatarUnavailable(msg) }"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]"
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid] || {}"
|
||||
:avatar-only="true"
|
||||
:hide-class-badge="true"
|
||||
:override-top-padding="'14px'"
|
||||
|
||||
@@ -245,12 +245,13 @@ import notifications from '@/mixins/notifications';
|
||||
import closeX from '../ui/closeX';
|
||||
|
||||
import copyIcon from '@/assets/svg/copy.svg';
|
||||
import copyToClipboard from '@/mixins/copyToClipboard';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
closeX,
|
||||
},
|
||||
mixins: [notifications],
|
||||
mixins: [notifications, copyToClipboard],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
@@ -287,17 +288,10 @@ export default {
|
||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||
},
|
||||
copyUsername () {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(this.user.auth.local.username);
|
||||
} else {
|
||||
const copyText = document.createElement('textarea');
|
||||
copyText.value = this.user.auth.local.username;
|
||||
document.body.appendChild(copyText);
|
||||
copyText.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(copyText);
|
||||
}
|
||||
this.text(this.$t('usernameCopied'));
|
||||
this.mixinCopyToClipboard(
|
||||
this.user.auth.local.username,
|
||||
this.$t('usernameCopied'),
|
||||
);
|
||||
},
|
||||
seekParty () {
|
||||
this.$store.dispatch('user:set', {
|
||||
|
||||
@@ -86,11 +86,6 @@
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: $red-50;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
v-if="invites.length > 0"
|
||||
class="row"
|
||||
>
|
||||
<div class="col-6 offset-3 nav">
|
||||
<div class="col-6 offset-3 nav mt-2 mb-3">
|
||||
<div
|
||||
class="nav-item"
|
||||
:class="{active: selectedPage === 'members'}"
|
||||
@@ -111,17 +111,18 @@
|
||||
:key="member._id"
|
||||
class="row"
|
||||
>
|
||||
<div class="col-11 no-padding-left">
|
||||
<div class="col-11 pl-0">
|
||||
<member-details
|
||||
:member="member"
|
||||
:class-badge-position="'next-to-name'"
|
||||
class="ml-4"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-1 actions">
|
||||
<b-dropdown right="right">
|
||||
<div
|
||||
slot="button-content"
|
||||
class="svg-icon inline dots"
|
||||
class="svg-icon inline dots pt-1"
|
||||
v-html="icons.dots"
|
||||
></div>
|
||||
<b-dropdown-item @click="sendMessage(member)">
|
||||
@@ -216,7 +217,7 @@
|
||||
:key="member._id"
|
||||
class="row"
|
||||
>
|
||||
<div class="col-11 no-padding-left">
|
||||
<div class="col-11 pl-0">
|
||||
<member-details :member="member" />
|
||||
</div>
|
||||
<div class="col-1 actions">
|
||||
@@ -259,10 +260,6 @@
|
||||
color: #878190;
|
||||
}
|
||||
|
||||
.no-padding-left {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
@@ -303,21 +300,15 @@
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding-top: 5em;
|
||||
|
||||
.b-dropdown {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
top: 8px;
|
||||
}
|
||||
.dots {
|
||||
height: 16px;
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
margin-left: -2em;
|
||||
margin-top: -2em;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
#members-modal_modal_body {
|
||||
@@ -353,8 +344,6 @@
|
||||
|
||||
.nav {
|
||||
font-weight: bold;
|
||||
margin-bottom: .5em;
|
||||
margin-top: .5em;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
:class-badge-position="'next-to-name'"
|
||||
:is-header="true"
|
||||
:disable-name-styling="true"
|
||||
class="mr-3"
|
||||
/>
|
||||
<div
|
||||
v-if="hasParty"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<creator-intro />
|
||||
<profileModal />
|
||||
<report-flag-modal />
|
||||
<report-member-modal />
|
||||
<send-gift-modal />
|
||||
<select-user-modal />
|
||||
<b-navbar
|
||||
@@ -732,6 +733,7 @@ import creatorIntro from '../creatorIntro';
|
||||
import notificationMenu from './notificationsDropdown';
|
||||
import profileModal from '../userMenu/profileModal';
|
||||
import reportFlagModal from '../chat/reportFlagModal';
|
||||
import reportMemberModal from '../members/reportMemberModal';
|
||||
import sendGiftModal from '@/components/payments/sendGiftModal';
|
||||
import selectUserModal from '@/components/payments/selectUserModal';
|
||||
import sync from '@/mixins/sync';
|
||||
@@ -744,6 +746,7 @@ export default {
|
||||
notificationMenu,
|
||||
profileModal,
|
||||
reportFlagModal,
|
||||
reportMemberModal,
|
||||
sendGiftModal,
|
||||
selectUserModal,
|
||||
userDropdown,
|
||||
|
||||
@@ -65,7 +65,7 @@ export default {
|
||||
}
|
||||
|
||||
await this.$store.dispatch('guilds:join', { groupId: group.id, type: 'guild' });
|
||||
this.$router.push({ name: 'guild', params: { groupId: group.id } });
|
||||
this.$router.push({ name: 'groupPlanDetailTaskInformation', params: { groupId: group.id } });
|
||||
},
|
||||
reject () {
|
||||
this.$store.dispatch('guilds:rejectInvite', { groupId: this.notification.data.id, type: 'guild' });
|
||||
|
||||
@@ -117,14 +117,14 @@ import * as quests from '@/../../common/script/content/quests';
|
||||
import { hasCompletedOnboarding } from '@/../../common/script/libs/onboarding';
|
||||
import notificationsIcon from '@/assets/svg/notifications.svg';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
import MessageCount from './messageCount';
|
||||
import MessageCount from './messageCount.functional.vue';
|
||||
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import successImage from '@/assets/svg/success.svg';
|
||||
import starBadge from '@/assets/svg/star-badge.svg';
|
||||
|
||||
// Notifications
|
||||
import CARD_RECEIVED from './notifications/cardReceived';
|
||||
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
|
||||
import CHALLENGE_INVITATION from './notifications/challengeInvitation.functional.vue';
|
||||
import GIFT_ONE_GET_ONE from './notifications/g1g1';
|
||||
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
|
||||
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
>{{ $t('achievements') }}</a>
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'site'}"
|
||||
:to="{name: 'general'}"
|
||||
>
|
||||
{{ $t('settings') }}
|
||||
</router-link>
|
||||
@@ -141,7 +141,7 @@
|
||||
import { mapState } from '@/libs/store';
|
||||
import userIcon from '@/assets/svg/user.svg';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
import MessageCount from './messageCount';
|
||||
import MessageCount from './messageCount.functional.vue';
|
||||
import { EVENTS } from '@/libs/events';
|
||||
import { PAGES } from '@/libs/consts';
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
<div v-if="currentDraggingEgg != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="'Pet_Egg_'+currentDraggingEgg.key"
|
||||
:class="`Pet_Egg_${currentDraggingEgg.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div class="popover-content">
|
||||
@@ -248,7 +248,7 @@
|
||||
<div v-if="currentDraggingEgg != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="'Pet_Egg_'+currentDraggingEgg.key"
|
||||
:class="`Pet_Egg_${currentDraggingEgg.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
@@ -266,7 +266,7 @@
|
||||
<div v-if="currentDraggingPotion != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="'Pet_HatchingPotion_'+currentDraggingPotion.key"
|
||||
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
@@ -285,7 +285,7 @@
|
||||
<div v-if="currentDraggingPotion != null">
|
||||
<div
|
||||
class="potion-icon"
|
||||
:class="'Pet_HatchingPotion_'+currentDraggingPotion.key"
|
||||
:class="`Pet_HatchingPotion_${currentDraggingPotion.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<span
|
||||
v-drag.food="item.key"
|
||||
class="item-content"
|
||||
:class="'Pet_Food_'+item.key"
|
||||
:class="`Pet_Food_${item.key}`"
|
||||
@itemDragEnd="dragend($event)"
|
||||
@itemDragStart="dragstart($event)"
|
||||
></span>
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
>
|
||||
<div class="potionEggGroup">
|
||||
<div class="potionEggBackground">
|
||||
<div :class="'Pet_HatchingPotion_'+hatchablePet.potionKey"></div>
|
||||
<div :class="`Pet_HatchingPotion_${hatchablePet.potionKey}`"></div>
|
||||
</div>
|
||||
<div class="potionEggBackground">
|
||||
<div :class="'Pet_Egg_'+hatchablePet.eggKey"></div>
|
||||
<div :class="`Pet_Egg_${hatchablePet.eggKey}`"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="title">
|
||||
|
||||
@@ -268,7 +268,7 @@
|
||||
<div v-if="currentDraggingFood != null">
|
||||
<div
|
||||
class="food-icon"
|
||||
:class="'Pet_Food_'+currentDraggingFood.key"
|
||||
:class="`Pet_Food_${currentDraggingFood.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
@@ -287,7 +287,7 @@
|
||||
<div v-if="currentDraggingFood != null">
|
||||
<div
|
||||
class="food-icon"
|
||||
:class="'Pet_Food_'+currentDraggingFood.key"
|
||||
:class="`Pet_Food_${currentDraggingFood.key}`"
|
||||
></div>
|
||||
<div class="popover">
|
||||
<div
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
class="member-details"
|
||||
:class="{ condensed, expanded, 'd-flex': isHeader, row: !isHeader, }"
|
||||
class="member-details d-flex"
|
||||
:class="{ condensed, expanded }"
|
||||
@click="showMemberModal(member)"
|
||||
>
|
||||
<div class="avatar-container" :class="{ 'col-4': !isHeader }">
|
||||
<div class="avatar-container">
|
||||
<avatar
|
||||
:member="member"
|
||||
:hide-class-badge="classBadgePosition !== 'under-avatar'"
|
||||
@@ -15,14 +15,17 @@
|
||||
</div>
|
||||
<div
|
||||
class="member-stats"
|
||||
:class="{'col-8': !expanded && !isHeader}"
|
||||
:class="{ 'mt-2': !isHeader }"
|
||||
>
|
||||
<div class="d-flex align-items-center profile-first-row">
|
||||
<class-badge
|
||||
v-if="classBadgePosition === 'next-to-name'"
|
||||
:member-class="member.stats.class"
|
||||
/>
|
||||
<div class="d-flex flex-column profile-name-character">
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
:class="{ 'ml-2': classBadgePosition === 'next-to-name' }"
|
||||
>
|
||||
<h3 class="character-name">
|
||||
<span v-if="member.contributor && member.contributor.level > 0 && !disableNameStyling">
|
||||
<user-link
|
||||
@@ -30,18 +33,22 @@
|
||||
:name="member.profile.name"
|
||||
:backer="member.backer"
|
||||
:contributor="member.contributor"
|
||||
:showBuffed="isBuffed"
|
||||
:context="'profile'"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>{{ member.profile.name }}</span>
|
||||
<div
|
||||
v-if="isBuffed"
|
||||
v-b-tooltip.hover.bottom="$t('buffed')"
|
||||
class="is-buffed"
|
||||
>
|
||||
<div v-else>
|
||||
<span>{{ member.profile.name }}</span>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.buff"
|
||||
></div>
|
||||
v-if="isBuffed"
|
||||
v-b-tooltip.hover.bottom="$t('buffed')"
|
||||
class="is-buffed ml-2 mt-n1"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.buff"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</h3>
|
||||
<div class="small-text character-level">
|
||||
@@ -98,9 +105,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.standard-page .member-details {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.member-stats {
|
||||
padding-left: 12px;
|
||||
padding-right: 24px;
|
||||
opacity: 1;
|
||||
transition: width 0.15s ease-out;
|
||||
}
|
||||
@@ -114,10 +124,6 @@
|
||||
color: $header-color;
|
||||
}
|
||||
|
||||
.profile-name-character {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.character-name {
|
||||
margin-bottom: 1px;
|
||||
color: $white;
|
||||
@@ -133,7 +139,6 @@
|
||||
height: 20px;
|
||||
background: $header-dark-background;
|
||||
display: inline-block;
|
||||
margin-left: 16px;
|
||||
vertical-align: middle;
|
||||
padding-top: 4px;
|
||||
|
||||
@@ -270,6 +275,9 @@ export default {
|
||||
methods: {
|
||||
percent,
|
||||
showMemberModal (member) {
|
||||
if (this.$route.name === 'userProfile' && this.$route.params?.userId === member._id) {
|
||||
return;
|
||||
}
|
||||
this.$router.push({ name: 'userProfile', params: { userId: member._id } });
|
||||
},
|
||||
},
|
||||
|
||||
199
website/client/src/components/members/reportMemberModal.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="report-profile"
|
||||
:title="$t('reportPlayer')"
|
||||
:hide-footer="!hasPermission(user, 'moderator')"
|
||||
size="md"
|
||||
>
|
||||
<div slot="modal-header">
|
||||
<h2 class="mt-2 mb-0"> {{ $t('reportPlayer') }} </h2>
|
||||
<close-x
|
||||
@close="close()"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<blockquote>
|
||||
<strong> {{ displayName }} </strong>
|
||||
<p class="mb-0"> {{ username }} </p>
|
||||
</blockquote>
|
||||
<div>
|
||||
<strong>{{ $t('whyReportingPlayer') }}</strong>
|
||||
<textarea
|
||||
v-model="reportComment"
|
||||
class="mt-2 form-control"
|
||||
:placeholder="$t('whyReportingPlayerPlaceholder')"
|
||||
></textarea>
|
||||
</div>
|
||||
<p
|
||||
class="mb-2"
|
||||
v-html="$t('playerReportModalBody', abuseFlagModalBody)">
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer text-center d-flex flex-column">
|
||||
<button
|
||||
class="btn btn-danger mx-auto mb-3"
|
||||
:disabled="!reportComment"
|
||||
:class="{ disabled: !reportComment }"
|
||||
@click="reportAbuse()"
|
||||
>
|
||||
{{ $t('report') }}
|
||||
</button>
|
||||
<a
|
||||
class="cancel-link"
|
||||
@click.prevent="close()"
|
||||
>{{ $t('cancel') }}</a>
|
||||
</div>
|
||||
<div
|
||||
slot="modal-footer"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
@click="resetFlags()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon icon-16 color my-auto mr-2"
|
||||
v-html="icons.report"
|
||||
></div>
|
||||
<a>Reset Flags</a>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
#report-profile {
|
||||
.modal-header {
|
||||
padding: 24px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0px 24px 24px 24px;
|
||||
}
|
||||
.modal-footer {
|
||||
color: $maroon-50;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border-top: none;
|
||||
height: 48px;
|
||||
background-color: rgba($red-500, 0.25);
|
||||
margin-top: -8px;
|
||||
padding: 0px;
|
||||
a {
|
||||
margin-top: 2px;
|
||||
color: $maroon-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
strong, p {
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: $maroon-100;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-radius: 4px;
|
||||
background-color: $gray-700;
|
||||
padding: .5rem 1rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
min-height: 106px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 1rem 1rem 0rem 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeX from '@/components/ui/closeX';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
import report from '@/assets/svg/report.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
closeX,
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
mixins: [notifications, userStateMixin],
|
||||
data () {
|
||||
const abuseFlagModalBody = {
|
||||
firstLinkStart: '<a href="/static/community-guidelines" target="_blank">',
|
||||
secondLinkStart: '<a href="/static/terms" target="_blank">',
|
||||
linkEnd: '</a>',
|
||||
};
|
||||
|
||||
return {
|
||||
abuseFlagModalBody,
|
||||
displayName: '',
|
||||
username: '',
|
||||
reportComment: '',
|
||||
icons: Object.freeze({
|
||||
report,
|
||||
}),
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica::report-profile', this.handleReport);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica::report-profile', this.handleReport);
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'report-profile');
|
||||
},
|
||||
async reportAbuse () {
|
||||
const result = await this.$store.dispatch('members:reportMember', {
|
||||
memberId: this.memberId,
|
||||
source: this.$route.fullPath,
|
||||
comment: this.reportComment,
|
||||
});
|
||||
if (result.status === 200) {
|
||||
this.text(this.$t('abuseReported'));
|
||||
|
||||
this.$root.$emit('habitica:report-profile-result', result.data.data);
|
||||
} else {
|
||||
this.error(result.statusText);
|
||||
}
|
||||
|
||||
this.close();
|
||||
},
|
||||
handleReport (data) {
|
||||
if (!data.memberId) return;
|
||||
this.displayName = data.displayName;
|
||||
this.username = `@${data.username}`;
|
||||
this.memberId = data.memberId;
|
||||
this.reportComment = '';
|
||||
this.$root.$emit('bv::show::modal', 'report-profile');
|
||||
},
|
||||
async resetFlags () {
|
||||
const result = await this.$store.dispatch('members:clearMemberFlags', {
|
||||
memberId: this.memberId,
|
||||
});
|
||||
if (result.status === 200) {
|
||||
this.text('Flags cleared.');
|
||||
} else {
|
||||
this.err(result.statusText);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -171,7 +171,12 @@
|
||||
</small>
|
||||
<h4 class="mt-3 mx-auto"> {{ $t('limitations') }}</h4>
|
||||
<small class="text-center">
|
||||
{{ $t('gemSaleLimitations', { eventStartMonth, eventStartOrdinal, eventEndOrdinal }) }}
|
||||
{{ $t('gemSaleLimitations', {
|
||||
eventStartMonth,
|
||||
eventStartOrdinal,
|
||||
eventEndMonth,
|
||||
eventEndOrdinal,
|
||||
}) }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
@@ -441,6 +446,9 @@ export default {
|
||||
eventStartOrdinal () {
|
||||
return moment(this.currentEvent.start).format('Do');
|
||||
},
|
||||
eventEndMonth () {
|
||||
return moment(this.currentEvent.end).format('MMMM');
|
||||
},
|
||||
eventEndOrdinal () {
|
||||
return moment(this.currentEvent.end).format('Do');
|
||||
},
|
||||
|
||||
@@ -22,18 +22,19 @@
|
||||
v-if="currentEvent && currentEvent.promo === 'g1g1'"
|
||||
class="g1g1-margin d-flex flex-column align-items-center"
|
||||
>
|
||||
<div
|
||||
class="svg-big-gift"
|
||||
v-once
|
||||
v-html="icons.bigGift"
|
||||
></div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-big-gift"
|
||||
v-html="icons.bigGift"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="d-flex flex-column align-items-center">
|
||||
v-else
|
||||
class="d-flex flex-column align-items-center"
|
||||
>
|
||||
<div
|
||||
class="svg-big-gift"
|
||||
v-once
|
||||
class="svg-big-gift"
|
||||
v-html="icons.bigGift"
|
||||
></div>
|
||||
</div>
|
||||
@@ -49,9 +50,10 @@
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="modal-close"
|
||||
@click="close()">
|
||||
v-else
|
||||
class="modal-close"
|
||||
@click="close()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.close"
|
||||
@@ -65,26 +67,15 @@
|
||||
name="selectUser"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="selectUser"
|
||||
v-model="userSearchTerm"
|
||||
class="form-control"
|
||||
type="text"
|
||||
ref="textBox"
|
||||
:placeholder="$t('usernameOrUserId')"
|
||||
:class="{
|
||||
'input-valid': foundUser._id,
|
||||
'is-invalid input-invalid': userNotFound,
|
||||
}"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="userSearchTerm.length > 0 && userNotFound"
|
||||
class="input-error text-center mt-2"
|
||||
>
|
||||
{{ $t('userWithUsernameOrUserIdNotFound') }}
|
||||
</div>
|
||||
<validated-text-input
|
||||
id="selectUser"
|
||||
v-model="userSearchTerm"
|
||||
:is-valid="foundUser._id"
|
||||
|
||||
:placeholder="$t('usernameOrUserId')"
|
||||
:invalid-issues="userInputInvalidIssues"
|
||||
/>
|
||||
|
||||
<div class="d-flex flex-column justify-content-center align-items-middle mt-3">
|
||||
<button
|
||||
class="btn btn-primary mx-auto mt-2"
|
||||
@@ -104,16 +95,12 @@
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-if="currentEvent && currentEvent.promo ==='g1g1'"
|
||||
class="g1g1-cancel d-flex justify-content-center"
|
||||
v-html="$t('cancel')"
|
||||
@click="close()"
|
||||
v-if="currentEvent && currentEvent.promo ==='g1g1'"
|
||||
class="g1g1-cancel d-flex justify-content-center"
|
||||
@click="close()"
|
||||
v-html="$t('cancel')"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</div>
|
||||
<div
|
||||
v-else>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,182 +108,179 @@
|
||||
slot="modal-footer"
|
||||
class="g1g1-fine-print text-center pt-3"
|
||||
>
|
||||
<strong>
|
||||
{{ $t ('howItWorks') }}
|
||||
<strong v-once>
|
||||
{{ $t('howItWorks') }}
|
||||
</strong>
|
||||
<p
|
||||
v-once
|
||||
class="mx-5 mt-1"
|
||||
>
|
||||
{{ $t ('g1g1HowItWorks') }}
|
||||
{{ $t('g1g1HowItWorks') }}
|
||||
</p>
|
||||
<strong>
|
||||
{{ $t ('limitations') }}
|
||||
<strong v-once>
|
||||
{{ $t('limitations') }}
|
||||
</strong>
|
||||
<p
|
||||
v-once
|
||||
class="mx-5 mt-1"
|
||||
>
|
||||
{{ $t ('g1g1Limitations') }}
|
||||
{{ $t('g1g1Limitations') }}
|
||||
</p>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
|
||||
#select-user-modal {
|
||||
.modal-content {
|
||||
width:448px;
|
||||
#select-user-modal {
|
||||
.modal-content {
|
||||
width: 448px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: 0rem;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0rem;
|
||||
|
||||
> * {
|
||||
margin: 0rem 0.25rem 0.25rem 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: 0rem;
|
||||
}
|
||||
body.modal-open .modal {
|
||||
display: flex !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 448px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 0rem;
|
||||
|
||||
> * {
|
||||
margin: 0rem 0.25rem 0.25rem 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
body.modal-open .modal {
|
||||
display: flex !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body.modal-open .modal .modal-dialog {
|
||||
margin: auto;
|
||||
}
|
||||
body.modal-open .modal .modal-dialog {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
a:not([href]) {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
}
|
||||
a:not([href]) {
|
||||
|
||||
#selectUser {
|
||||
width: 22rem;
|
||||
border: 0px;
|
||||
color: $gray-50;
|
||||
}
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.g1g1 {
|
||||
background-image: url('~@/assets/images/g1g1-send.png');
|
||||
background-size: 446px 152px;
|
||||
width: 446px;
|
||||
height: 152px;
|
||||
margin: -16px 0px 0px -16px;
|
||||
border-radius: 4.8px 4.8px 0px 0px;
|
||||
padding: 24px;
|
||||
#selectUser {
|
||||
width: 22rem;
|
||||
border: 0px;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.g1g1 {
|
||||
background-image: url('~@/assets/images/g1g1-send.png');
|
||||
background-size: 446px 152px;
|
||||
width: 446px;
|
||||
height: 152px;
|
||||
margin: -16px 0px 0px -16px;
|
||||
border-radius: 4.8px 4.8px 0px 0px;
|
||||
padding: 24px;
|
||||
color: $white;
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.4;
|
||||
color: $white;
|
||||
|
||||
h1 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.4;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
margin-left: 4rem;
|
||||
margin-right: 4rem;
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
.g1g1-margin {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.g1g1-cancel {
|
||||
margin-top: 16px;
|
||||
color: $blue-10;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.g1g1-fine-print {
|
||||
color: $gray-100;
|
||||
background-color: $gray-700;
|
||||
p {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
margin-left: 4rem;
|
||||
margin-right: 4rem;
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
.g1g1-modal-close {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
.g1g1-margin {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.g1g1-svg-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
.g1g1-cancel {
|
||||
margin-top: 16px;
|
||||
color: $blue-10;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
& ::v-deep svg path {
|
||||
fill: #FFFFFF;
|
||||
}
|
||||
.g1g1-fine-print {
|
||||
color: $gray-100;
|
||||
background-color: $gray-700;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.g1g1-modal-close {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.g1g1-svg-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
||||
& ::v-deep svg path {
|
||||
fill: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.g1g1-modal-dialog {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: $red-50;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group:focus-within {
|
||||
border-color: $purple-500;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
color: $purple-300;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.svg-big-gift {
|
||||
width: 176px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.svg-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
.g1g1-modal-dialog {
|
||||
margin-top: 10vh;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.input-group:focus-within {
|
||||
border-color: $purple-500;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
color: $purple-300;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.svg-big-gift {
|
||||
width: 176px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.svg-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -308,8 +292,10 @@ import isUUID from 'validator/lib/isUUID';
|
||||
import { mapState } from '@/libs/store';
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
import bigGiftIcon from '@/assets/svg/big-gift.svg';
|
||||
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
|
||||
|
||||
export default {
|
||||
components: { ValidatedTextInput },
|
||||
data () {
|
||||
return {
|
||||
userNotFound: false,
|
||||
@@ -332,6 +318,12 @@ export default {
|
||||
if (this.userSearchTerm.length < 1) return true;
|
||||
return typeof this.foundUser._id === 'undefined';
|
||||
},
|
||||
|
||||
userInputInvalidIssues () {
|
||||
return this.userSearchTerm.length > 0 && this.userNotFound
|
||||
? [this.$t('userWithUsernameOrUserIdNotFound')]
|
||||
: [''];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
userSearchTerm: {
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
<template>
|
||||
<div class="row standard-page">
|
||||
<div class="col-6">
|
||||
<h2>{{ $t('API') }}</h2>
|
||||
<p>{{ $t('APIText') }}</p>
|
||||
<div class="section">
|
||||
<h6>{{ $t('userId') }}</h6>
|
||||
<pre class="prettyprint">{{ user.id }}</pre>
|
||||
<h6>{{ $t('APIToken') }}</h6>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
@click="showApiToken = !showApiToken"
|
||||
>
|
||||
{{ $t(`${showApiToken ? 'hide' : 'show'}APIToken`) }}
|
||||
</button>
|
||||
<pre
|
||||
v-if="showApiToken"
|
||||
class="prettyprint ml-4 mb-0"
|
||||
>{{ apiToken }}</pre>
|
||||
</div>
|
||||
<p v-html="$t('APITokenWarning', { hrefTechAssistanceEmail })"></p>
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>{{ $t('thirdPartyApps') }}</h3>
|
||||
<p v-html="$t('thirdPartyTools')"></p>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h2>{{ $t('webhooks') }}</h2>
|
||||
<p v-html="$t('webhooksInfo')"></p>
|
||||
<table class="table table-striped">
|
||||
<thead v-if="user.webhooks.length">
|
||||
<tr>
|
||||
<th>{{ $t('enabled') }}</th>
|
||||
<th>{{ $t('webhookURL') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(webhook, index) in user.webhooks"
|
||||
:key="webhook.id"
|
||||
>
|
||||
<td>
|
||||
<input
|
||||
v-model="webhook.enabled"
|
||||
type="checkbox"
|
||||
@change="saveWebhook(webhook, index)"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="webhook.url"
|
||||
class="form-control"
|
||||
type="url"
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
class="btn btn-danger checklist-icons mr-2"
|
||||
@click="deleteWebhook(webhook, index)"
|
||||
>
|
||||
<span
|
||||
class="glyphicon glyphicon-trash"
|
||||
:tooltip="$t('delete')"
|
||||
> {{ $t('delete') }} </span>
|
||||
</div>
|
||||
<div
|
||||
class="btn btn-primary checklist-icons"
|
||||
@click="saveWebhook(webhook, index)"
|
||||
>
|
||||
{{ $t('subUpdateTitle') }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group col-sm-10">
|
||||
<input
|
||||
v-model="newWebhook.url"
|
||||
class="form-control"
|
||||
type="url"
|
||||
:placeholder="$t('webhookURL')"
|
||||
>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button
|
||||
class="btn btn-sm btn-primary"
|
||||
type="submit"
|
||||
@click="addWebhook(newWebhook.url)"
|
||||
>
|
||||
{{ $t('add') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.section {
|
||||
margin-top: 2em;
|
||||
}
|
||||
li span
|
||||
{
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import uuid from '@/../../common/script/libs/uuid';
|
||||
// @TODO: env.EMAILS.TECH_ASSISTANCE_EMAIL
|
||||
const TECH_ASSISTANCE_EMAIL = 'admin@habitica.com';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
newWebhook: {
|
||||
url: '',
|
||||
},
|
||||
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
|
||||
showApiToken: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data', credentials: 'credentials' }),
|
||||
apiToken () {
|
||||
return this.credentials.API_TOKEN;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('settings'),
|
||||
subSection: this.$t('API'),
|
||||
});
|
||||
window.addEventListener('message', this.receiveMessage, false);
|
||||
},
|
||||
destroy () {
|
||||
window.removeEventListener('message', this.receiveMessage);
|
||||
},
|
||||
methods: {
|
||||
receiveMessage (eventFrom) {
|
||||
if (eventFrom.origin !== 'https://www.spritely.app') return;
|
||||
|
||||
const creds = {
|
||||
userId: this.user._id,
|
||||
apiToken: this.credentials.API_TOKEN,
|
||||
};
|
||||
eventFrom.source.postMessage(creds, eventFrom.origin);
|
||||
},
|
||||
async addWebhook (url) {
|
||||
const webhookInfo = {
|
||||
id: uuid(),
|
||||
type: 'taskActivity',
|
||||
options: {
|
||||
created: false,
|
||||
updated: false,
|
||||
deleted: false,
|
||||
scored: true,
|
||||
},
|
||||
url,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const webhook = await this.$store.dispatch('user:addWebhook', { webhookInfo });
|
||||
this.user.webhooks.push(webhook);
|
||||
|
||||
this.newWebhook.url = '';
|
||||
},
|
||||
async saveWebhook (webhook, index) {
|
||||
delete webhook._editing;
|
||||
const updatedWebhook = await this.$store.dispatch('user:updateWebhook', { webhook });
|
||||
this.user.webhooks[index] = updatedWebhook;
|
||||
},
|
||||
async deleteWebhook (webhook, index) {
|
||||
delete webhook._editing;
|
||||
await this.$store.dispatch('user:deleteWebhook', { webhook });
|
||||
this.user.webhooks.splice(index, 1);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h2>{{ $t('dataExport') }}</h2>
|
||||
<small>{{ $t('saveData') }}</small>
|
||||
<h4>{{ $t('habitHistory') }}</h4>
|
||||
{{ $t('exportHistory') }}
|
||||
<a href="/export/history.csv">{{ $t('csv') }}</a>
|
||||
<h4>{{ $t('userData') }}</h4>
|
||||
{{ $t('exportUserData') }}
|
||||
<a href="/export/userdata.xml">{{ $t('xml') }}</a>
|
||||
<a href="/export/userdata.json">{{ $t('json') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('settings'),
|
||||
subSection: this.$t('dataExport'),
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<h5>{{ $t('dayStartAdjustment') }}</h5>
|
||||
<div class="mb-4">
|
||||
{{ $t('customDayStartInfo1') }}
|
||||
</div>
|
||||
<h3 v-once>{{ $t('adjustment') }}</h3>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="">
|
||||
<select
|
||||
v-model="newDayStart"
|
||||
class="form-control"
|
||||
>
|
||||
<option
|
||||
v-for="option in dayStartOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-primary full-width mt-3"
|
||||
:disabled="newDayStart === user.preferences.dayStart"
|
||||
@click="openDayStartModal()"
|
||||
>
|
||||
{{ $t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<small>
|
||||
<p v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})"></p>
|
||||
<p v-html="$t('timezoneInfo')"></p>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import getUtcOffset from '../../../../common/script/fns/getUtcOffset';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
name: 'dayStartAdjustment',
|
||||
data () {
|
||||
const dayStartOptions = [];
|
||||
for (let number = 0; number <= 12; number += 1) {
|
||||
const meridian = number < 12 ? 'AM' : 'PM';
|
||||
const hour = number % 12;
|
||||
const timeWithMeridian = `(${hour || 12}:00 ${meridian})`;
|
||||
const option = {
|
||||
value: number,
|
||||
name: `+${number} hours ${timeWithMeridian}`,
|
||||
};
|
||||
|
||||
if (number === 0) {
|
||||
option.name = `Default ${timeWithMeridian}`;
|
||||
}
|
||||
|
||||
dayStartOptions.push(option);
|
||||
}
|
||||
|
||||
return {
|
||||
newDayStart: 0,
|
||||
dayStartOptions,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.newDayStart = this.user.preferences.dayStart;
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
timezoneOffsetToUtc () {
|
||||
const offsetString = moment().utcOffset(getUtcOffset(this.user)).format('Z');
|
||||
return `UTC${offsetString}`;
|
||||
},
|
||||
dayStart () {
|
||||
return this.user.preferences.dayStart;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async saveDayStart () {
|
||||
this.user.preferences.dayStart = this.newDayStart;
|
||||
await axios.post('/api/v4/user/custom-day-start', {
|
||||
dayStart: this.newDayStart,
|
||||
});
|
||||
// @TODO
|
||||
// Notification.text(response.data.data.message);
|
||||
},
|
||||
openDayStartModal () {
|
||||
const nextCron = this.calculateNextCron();
|
||||
// @TODO: Add generic modal
|
||||
if (!window.confirm(this.$t('sureChangeCustomDayStartTime', { time: nextCron }))) return; // eslint-disable-line no-alert
|
||||
this.saveDayStart();
|
||||
// $rootScope.openModal('change-day-start', { scope: $scope });
|
||||
},
|
||||
calculateNextCron () {
|
||||
let nextCron = moment()
|
||||
.hours(this.newDayStart)
|
||||
.minutes(0)
|
||||
.seconds(0)
|
||||
.milliseconds(0);
|
||||
|
||||
const currentHour = moment().format('H');
|
||||
if (currentHour >= this.newDayStart) {
|
||||
nextCron = nextCron.add(1, 'day');
|
||||
}
|
||||
|
||||
return nextCron.format(`${this.user.preferences.dateFormat.toUpperCase()} @ h:mm a`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="delete"
|
||||
:title="$t('deleteAccount')"
|
||||
:hide-footer="true"
|
||||
size="md"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<br>
|
||||
<strong v-if="user.auth.local.has_password">{{ $t('deleteLocalAccountText') }}</strong>
|
||||
<strong
|
||||
v-if="!user.auth.local.has_password"
|
||||
>{{ $t('deleteSocialAccountText', {magicWord: 'DELETE'}) }}</strong>
|
||||
<div class="row mt-3">
|
||||
<div class="col-6">
|
||||
<input
|
||||
v-model="password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div
|
||||
id="feedback"
|
||||
class="col-12 form-group"
|
||||
>
|
||||
<label for="feedbackTextArea">{{ $t('feedback') }}</label>
|
||||
<textarea
|
||||
id="feedbackTextArea"
|
||||
v-model="feedback"
|
||||
class="form-control"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('neverMind') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
:disabled="!password"
|
||||
@click="deleteAccount()"
|
||||
>
|
||||
{{ $t('deleteDo') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
password: '',
|
||||
feedback: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'delete');
|
||||
},
|
||||
async deleteAccount () {
|
||||
await axios.delete('/api/v4/user', {
|
||||
data: {
|
||||
password: this.password,
|
||||
feedback: this.feedback,
|
||||
},
|
||||
});
|
||||
localStorage.clear();
|
||||
window.location.href = '/static/home';
|
||||
this.$root.$emit('bv::hide::modal', 'delete');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,158 +0,0 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<secondary-menu class="col-12">
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'site'}"
|
||||
exact="exact"
|
||||
:class="{'active': $route.name === 'site'}"
|
||||
>
|
||||
{{ $t('site') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'api'}"
|
||||
:class="{'active': $route.name === 'api'}"
|
||||
>
|
||||
{{ $t('API') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'dataExport'}"
|
||||
:class="{'active': $route.name === 'dataExport'}"
|
||||
>
|
||||
{{ $t('dataExport') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'promoCode'}"
|
||||
:class="{'active': $route.name === 'promoCode'}"
|
||||
>
|
||||
{{ $t('promoCode') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'subscription'}"
|
||||
:class="{'active': $route.name === 'subscription'}"
|
||||
>
|
||||
{{ $t('subscription') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="hasPermission(user, 'userSupport')"
|
||||
class="nav-link"
|
||||
:to="{name: 'transactions'}"
|
||||
:class="{'active': $route.name === 'transactions'}"
|
||||
>
|
||||
{{ $t('transactions') }}
|
||||
</router-link>
|
||||
<router-link
|
||||
class="nav-link"
|
||||
:to="{name: 'notifications'}"
|
||||
:class="{'active': $route.name === 'notifications'}"
|
||||
>
|
||||
{{ $t('notifications') }}
|
||||
</router-link>
|
||||
</secondary-menu>
|
||||
<div
|
||||
v-if="$route.name === 'subscription' && promo === 'g1g1'"
|
||||
class="g1g1-banner d-flex justify-content-center"
|
||||
@click="showSelectUser"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-gifts left-gift"
|
||||
v-html="icons.gifts"
|
||||
>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center text-center">
|
||||
<strong
|
||||
class="mt-auto mb-1"
|
||||
> {{ $t('g1g1Event') }} </strong>
|
||||
<p
|
||||
class="mb-auto"
|
||||
>
|
||||
{{ $t('g1g1Details') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-gifts right-gift"
|
||||
v-html="icons.gifts"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
strong {
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.g1g1-banner {
|
||||
color: $white;
|
||||
width: 100%;
|
||||
height: 5.75rem;
|
||||
background-image: linear-gradient(90deg, $teal-50 0%, $purple-400 100%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left-gift {
|
||||
margin: auto 3rem auto auto;
|
||||
}
|
||||
|
||||
.right-gift {
|
||||
margin: auto auto auto 3rem;
|
||||
filter: flipH;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.svg-gifts {
|
||||
width: 3.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import { mapState } from '@/libs/store';
|
||||
import SecondaryMenu from '@/components/secondaryMenu';
|
||||
import gifts from '@/assets/svg/gifts-vertical.svg';
|
||||
import { userStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gifts,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
currentEvent () {
|
||||
return find(this.currentEventList, event => Boolean(event.promo));
|
||||
},
|
||||
promo () {
|
||||
if (!this.currentEvent || !this.currentEvent.promo) return 'none';
|
||||
return this.currentEvent.promo;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showSelectUser () {
|
||||
this.$root.$emit('bv::show::modal', 'select-user-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,143 +0,0 @@
|
||||
<template>
|
||||
<div class="row standard-page">
|
||||
<div class="col-12">
|
||||
<h1>{{ $t('notifications') }}</h1>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="user.preferences.pushNotifications.unsubscribeFromAll"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('pushNotifications', 'unsubscribeFromAll')"
|
||||
>
|
||||
<span>{{ $t('unsubscribeAllPush') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input
|
||||
v-model="user.preferences.emailNotifications.unsubscribeFromAll"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('emailNotifications', 'unsubscribeFromAll')"
|
||||
>
|
||||
<span>{{ $t('unsubscribeAllEmails') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<small>{{ $t('unsubscribeAllEmailsText') }}</small>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<table class="table">
|
||||
<tr>
|
||||
<td></td>
|
||||
<th>
|
||||
<span>{{ $t('email') }}</span>
|
||||
</th>
|
||||
<th>
|
||||
<span>{{ $t('push') }}</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr
|
||||
v-for="notification in notificationsIds"
|
||||
:key="notification"
|
||||
>
|
||||
<td>
|
||||
<span>{{ $t(notification) }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="user.preferences.emailNotifications[notification]"
|
||||
type="checkbox"
|
||||
@change="set('emailNotifications', notification)"
|
||||
>
|
||||
</td>
|
||||
<td v-if="onlyEmailsIds.indexOf(notification) === -1">
|
||||
<input
|
||||
v-model="user.preferences.pushNotifications[notification]"
|
||||
type="checkbox"
|
||||
@change="set('pushNotifications', notification)"
|
||||
>
|
||||
</td><td v-else>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
|
||||
export default {
|
||||
mixins: [notificationsMixin],
|
||||
data () {
|
||||
return {
|
||||
notificationsIds: Object.freeze([
|
||||
'majorUpdates',
|
||||
'onboarding',
|
||||
'newPM',
|
||||
'wonChallenge',
|
||||
'giftedGems',
|
||||
'giftedSubscription',
|
||||
'invitedParty',
|
||||
'invitedGuild',
|
||||
'kickedGroup',
|
||||
'questStarted',
|
||||
'invitedQuest',
|
||||
'importantAnnouncements',
|
||||
'weeklyRecaps',
|
||||
'subscriptionReminders',
|
||||
]),
|
||||
// list of email-only notifications
|
||||
onlyEmailsIds: Object.freeze([
|
||||
'kickedGroup',
|
||||
'importantAnnouncements',
|
||||
'weeklyRecaps',
|
||||
'onboarding',
|
||||
'subscriptionReminders',
|
||||
]),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
async mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('settings'),
|
||||
subSection: this.$t('notifications'),
|
||||
});
|
||||
// If ?unsubFrom param is passed with valid email type,
|
||||
// automatically unsubscribe users from that email and
|
||||
// show an alert
|
||||
|
||||
// A simple object to map the key stored in the db (user.preferences.emailNotification[key])
|
||||
// to its string id but ONLY when the preferences' key and the string key don't match
|
||||
const MAP_PREF_TO_EMAIL_STRING = {
|
||||
importantAnnouncements: 'inactivityEmails',
|
||||
};
|
||||
|
||||
const { unsubFrom } = this.$route.query;
|
||||
|
||||
if (unsubFrom) {
|
||||
await this.$store.dispatch('user:set', {
|
||||
[`preferences.emailNotifications.${unsubFrom}`]: false,
|
||||
});
|
||||
|
||||
const emailTypeString = this.$t(MAP_PREF_TO_EMAIL_STRING[unsubFrom] || unsubFrom);
|
||||
this.text(this.$t('correctlyUnsubscribedEmailType', { emailType: emailTypeString }));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
set (preferenceType, notification) {
|
||||
const settings = {};
|
||||
settings[`preferences.${preferenceType}.${notification}`] = this.user.preferences[preferenceType][notification];
|
||||
this.$store.dispatch('user:set', settings);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,117 +0,0 @@
|
||||
<template>
|
||||
<div class="row standard-page">
|
||||
<div class="col-md-6">
|
||||
<h2>{{ $t('promoCode') }}</h2>
|
||||
<div
|
||||
class="form-inline"
|
||||
role="form"
|
||||
>
|
||||
<input
|
||||
v-model="couponCode"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('promoPlaceholder')"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="enterCoupon()"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<small>{{ $t('couponText') }}</small>
|
||||
</div>
|
||||
<div v-if="user.permissions.coupons">
|
||||
<hr>
|
||||
<h4>{{ $t('generateCodes') }}</h4>
|
||||
<div
|
||||
class="form"
|
||||
role="form"
|
||||
>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="codes.event"
|
||||
class="form-control"
|
||||
type="text"
|
||||
placeholder="Event code (eg, 'wondercon')"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="codes.count"
|
||||
class="form-control"
|
||||
type="number"
|
||||
placeholder="Number of codes to generate (eg, 250)"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
@click="generateCodes(codes)"
|
||||
>
|
||||
{{ $t('generate') }}
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
:href="getCodesUrl"
|
||||
>{{ $t('getCodes') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
data () {
|
||||
return {
|
||||
codes: {
|
||||
event: '',
|
||||
count: '',
|
||||
},
|
||||
couponCode: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data', credentials: 'credentials' }),
|
||||
getCodesUrl () {
|
||||
if (!this.user) return '';
|
||||
return '/api/v4/coupons';
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('settings'),
|
||||
subSection: this.$t('promoCode'),
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
generateCodes () {
|
||||
// $http.post(ApiUrl.get() + '/api/v2/coupons/generate/
|
||||
// '+codes.event+'?count='+(codes.count || 1))
|
||||
// .success(function(res,code){
|
||||
// $scope._codes = {};
|
||||
// if (code!==200) return;
|
||||
// window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+
|
||||
// '&apiToken='+User.settings.auth.apiToken;
|
||||
// })
|
||||
},
|
||||
async enterCoupon () {
|
||||
const code = await axios.post(`/api/v4/coupons/enter/${this.couponCode}`);
|
||||
if (!code) return;
|
||||
|
||||
this.$store.state.user.data = code.data.data;
|
||||
|
||||
this.text(this.$t('promoCodeApplied'));
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,46 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="reset"
|
||||
:title="$t('resetAccount')"
|
||||
:hide-footer="true"
|
||||
size="md"
|
||||
>
|
||||
<p>{{ $t('resetText1') }}</p>
|
||||
<p>{{ $t('resetText2') }}</p>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('neverMind') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="reset()"
|
||||
>
|
||||
{{ $t('resetDo') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'reset');
|
||||
},
|
||||
async reset () {
|
||||
await axios.post('/api/v4/user/reset');
|
||||
this.$router.push('/');
|
||||
setTimeout(() => window.location.reload(true), 100);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,210 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="restore"
|
||||
:title="$t('fixValues')"
|
||||
:hide-footer="true"
|
||||
size="lg"
|
||||
>
|
||||
<p>{{ $t('fixValuesText1') }}</p>
|
||||
<p>{{ $t('fixValuesText2') }}</p>
|
||||
<div class="form-horizontal">
|
||||
<h3>{{ $t('stats') }}</h3>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="control-label">{{ $t('health') }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="restoreValues.stats.hp"
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="any"
|
||||
data-for="stats.hp"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="control-label">{{ $t('experience') }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="restoreValues.stats.exp"
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="any"
|
||||
data-for="stats.exp"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="control-label">{{ $t('gold') }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="restoreValues.stats.gp"
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="any"
|
||||
data-for="stats.gp"
|
||||
>
|
||||
</div>
|
||||
<!--input.form-control(type='number',
|
||||
step="any", data-for='stats.gp', v-model='restoreValues.stats.gp',disabled)-->
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="control-label">{{ $t('mana') }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="restoreValues.stats.mp"
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="any"
|
||||
data-for="stats.mp"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="control-label">{{ $t('level') }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="restoreValues.stats.lvl"
|
||||
class="form-control"
|
||||
type="number"
|
||||
data-for="stats.lvl"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<h3>{{ $t('achievements') }}</h3>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-3">
|
||||
<label class="control-label">{{ $t('fix21Streaks') }}</label>
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
v-model="restoreValues.achievements.streak"
|
||||
class="form-control"
|
||||
type="number"
|
||||
data-for="achievements.streak"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-danger"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('discardChanges') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="restore()"
|
||||
>
|
||||
{{ $t('saveAndClose') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import clone from 'lodash/clone';
|
||||
import { MAX_LEVEL_HARD_CAP } from '@/../../common/script/constants';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
restoreValues: {
|
||||
stats: {
|
||||
hp: 0,
|
||||
mp: 0,
|
||||
gp: 0,
|
||||
exp: 0,
|
||||
lvl: 0,
|
||||
},
|
||||
achievements: {
|
||||
streak: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
mounted () {
|
||||
this.restoreValues.stats = clone(this.user.stats);
|
||||
this.restoreValues.achievements.streak = clone(this.user.achievements.streak);
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.validateInputs();
|
||||
this.$root.$emit('bv::hide::modal', 'restore');
|
||||
},
|
||||
restore () {
|
||||
if (!this.validateInputs()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.restoreValues.stats.lvl > MAX_LEVEL_HARD_CAP) {
|
||||
this.restoreValues.stats.lvl = MAX_LEVEL_HARD_CAP;
|
||||
}
|
||||
|
||||
const userChangedLevel = this.restoreValues.stats.lvl !== this.user.stats.lvl;
|
||||
const userDidNotChangeExp = this.restoreValues.stats.exp === this.user.stats.exp;
|
||||
if (userChangedLevel && userDidNotChangeExp) this.restoreValues.stats.exp = 0;
|
||||
|
||||
this.user.stats = clone(this.restoreValues.stats);
|
||||
this.user.achievements.streak = clone(this.restoreValues.achievements.streak);
|
||||
|
||||
const settings = {
|
||||
'stats.hp': Number(this.restoreValues.stats.hp),
|
||||
'stats.exp': Number(this.restoreValues.stats.exp),
|
||||
'stats.gp': Number(this.restoreValues.stats.gp),
|
||||
'stats.lvl': Number(this.restoreValues.stats.lvl),
|
||||
'stats.mp': Number(this.restoreValues.stats.mp),
|
||||
'achievements.streak': Number(this.restoreValues.achievements.streak),
|
||||
};
|
||||
|
||||
this.$store.dispatch('user:set', settings);
|
||||
this.$root.$emit('bv::hide::modal', 'restore');
|
||||
},
|
||||
validateInputs () {
|
||||
const canRestore = ['hp', 'exp', 'gp', 'mp'];
|
||||
let valid = true;
|
||||
|
||||
for (const stat of canRestore) {
|
||||
if (this.restoreValues.stats[stat] === ''
|
||||
|| this.restoreValues.stats[stat] < 0
|
||||
) {
|
||||
this.restoreValues.stats[stat] = this.user.stats[stat];
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
const inputLevel = Number(this.restoreValues.stats.lvl);
|
||||
if (this.restoreValues.stats.lvl === ''
|
||||
|| !Number.isInteger(inputLevel)
|
||||
|| inputLevel < 1) {
|
||||
this.restoreValues.stats.lvl = this.user.stats.lvl;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
const inputStreak = Number(this.restoreValues.achievements.streak);
|
||||
if (this.restoreValues.achievements.streak === ''
|
||||
|| !Number.isInteger(inputStreak)
|
||||
|| inputStreak < 0) {
|
||||
this.restoreValues.achievements.streak = this.user.achievements.streak;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,859 +0,0 @@
|
||||
<template>
|
||||
<div class="row standard-page">
|
||||
<restore-modal />
|
||||
<reset-modal />
|
||||
<delete-modal />
|
||||
<h1 class="col-12">
|
||||
{{ $t('settings') }}
|
||||
</h1>
|
||||
<div class="col-sm-6">
|
||||
<div class="sleep">
|
||||
<h5>{{ $t('pauseDailies') }}</h5>
|
||||
<h4>{{ $t('sleepDescription') }}</h4>
|
||||
<ul>
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet1') }}
|
||||
</li>
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet2') }}
|
||||
</li>
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet3') }}
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
v-if="!user.preferences.sleep"
|
||||
v-once
|
||||
class="sleep btn btn-primary btn-block pause-button"
|
||||
@click="toggleSleep()"
|
||||
>
|
||||
{{ $t('pauseDailies') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="user.preferences.sleep"
|
||||
v-once
|
||||
class="btn btn-secondary btn-block pause-button"
|
||||
@click="toggleSleep()"
|
||||
>
|
||||
{{ $t('unpauseDailies') }}
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-horizontal">
|
||||
<h5>{{ $t('language') }}</h5>
|
||||
<select
|
||||
class="form-control"
|
||||
:value="user.preferences.language"
|
||||
@change="changeLanguage($event)"
|
||||
>
|
||||
<option
|
||||
v-for="lang in availableLanguages"
|
||||
:key="lang.code"
|
||||
:value="lang.code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</select>
|
||||
<small>
|
||||
{{ $t('americanEnglishGovern') }}
|
||||
<br>
|
||||
<strong v-html="$t('helpWithTranslation')"></strong>
|
||||
</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-horizontal">
|
||||
<h5>{{ $t('dateFormat') }}</h5>
|
||||
<select
|
||||
v-model="user.preferences.dateFormat"
|
||||
class="form-control"
|
||||
@change="set('dateFormat')"
|
||||
>
|
||||
<option
|
||||
v-for="dateFormat in availableFormats"
|
||||
:key="dateFormat"
|
||||
:value="dateFormat"
|
||||
>
|
||||
{{ dateFormat }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<h5>{{ $t('audioTheme') }}</h5>
|
||||
<select
|
||||
v-model="user.preferences.sound"
|
||||
class="form-control"
|
||||
@change="changeAudioTheme"
|
||||
>
|
||||
<option
|
||||
v-for="sound in availableAudioThemes"
|
||||
:key="sound"
|
||||
:value="sound"
|
||||
>
|
||||
{{ $t(`audioTheme_${sound}`) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary btn-xs"
|
||||
@click="playAudio"
|
||||
>
|
||||
{{ $t('demo') }}
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div
|
||||
v-if="hasClass"
|
||||
class="form-horizontal"
|
||||
>
|
||||
<h5>{{ $t('characterBuild') }}</h5>
|
||||
<h6 v-once>
|
||||
{{ $t('class') + ': ' }}
|
||||
<!-- @TODO: what is classText-->
|
||||
<!-- span(v-if='classText') {{ classText }} -->
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-danger btn-xs"
|
||||
@click="changeClassForUser(true)"
|
||||
>
|
||||
{{ $t('changeClass') }}
|
||||
</button>
|
||||
<small class="cost">
|
||||
3 {{ $t('gems') }}
|
||||
<!-- @TODO add icon span.Pet_Currency_Gem1x.inline-gems-->
|
||||
</small>
|
||||
</h6>
|
||||
<hr>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="checkbox"
|
||||
id="preferenceAdvancedCollapsed"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
v-model="user.preferences.advancedCollapsed"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('advancedCollapsed')"
|
||||
>
|
||||
<span class="hint">
|
||||
{{ $t('startAdvCollapsed') }}
|
||||
</span>
|
||||
<b-popover
|
||||
target="preferenceAdvancedCollapsed"
|
||||
triggers="hover focus"
|
||||
placement="right"
|
||||
:prevent-overflow="false"
|
||||
:content="$t('startAdvCollapsedPop')"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="party.memberCount === 1"
|
||||
class="checkbox"
|
||||
id="preferenceDisplayInviteAtOneMember"
|
||||
>
|
||||
<label>
|
||||
<input
|
||||
v-model="user.preferences.displayInviteToPartyWhenPartyIs1"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('displayInviteToPartyWhenPartyIs1')"
|
||||
>
|
||||
<span class="hint">
|
||||
{{ $t('displayInviteToPartyWhenPartyIs1') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<input
|
||||
v-model="user.preferences.suppressModals.levelUp"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('suppressModals', 'levelUp')"
|
||||
>
|
||||
<label>{{ $t('suppressLevelUpModal') }}</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<input
|
||||
v-model="user.preferences.suppressModals.hatchPet"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('suppressModals', 'hatchPet')"
|
||||
>
|
||||
<label>{{ $t('suppressHatchPetModal') }}</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<input
|
||||
v-model="user.preferences.suppressModals.raisePet"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('suppressModals', 'raisePet')"
|
||||
>
|
||||
<label>{{ $t('suppressRaisePetModal') }}</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<input
|
||||
v-model="user.preferences.suppressModals.streak"
|
||||
type="checkbox"
|
||||
class="mr-2"
|
||||
@change="set('suppressModals', 'streak')"
|
||||
>
|
||||
<label>{{ $t('suppressStreakModal') }}</label>
|
||||
</div>
|
||||
<hr>
|
||||
<button
|
||||
id="buttonShowBailey"
|
||||
class="btn btn-primary mr-2 mb-2"
|
||||
@click="showBailey()"
|
||||
>
|
||||
{{ $t('showBailey') }}
|
||||
<b-popover
|
||||
target="buttonShowBailey"
|
||||
triggers="hover focus"
|
||||
placement="right"
|
||||
:prevent-overflow="false"
|
||||
:content="$t('showBaileyPop')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
id="buttonFCV"
|
||||
class="btn btn-primary mr-2 mb-2"
|
||||
@click="openRestoreModal()"
|
||||
>
|
||||
{{ $t('fixVal') }}
|
||||
<b-popover
|
||||
target="buttonFCV"
|
||||
triggers="hover focus"
|
||||
placement="right"
|
||||
:prevent-overflow="false"
|
||||
:content="$t('fixValPop')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="user.preferences.disableClasses == true"
|
||||
id="buttonEnableClasses"
|
||||
class="btn btn-primary mb-2"
|
||||
@click="changeClassForUser(false)"
|
||||
>
|
||||
{{ $t('enableClass') }}
|
||||
<b-popover
|
||||
target="buttonEnableClasses"
|
||||
triggers="hover focus"
|
||||
placement="right"
|
||||
:prevent-overflow="false"
|
||||
:content="$t('enableClassPop')"
|
||||
/>
|
||||
</button>
|
||||
<hr>
|
||||
<day-start-adjustment />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h2>{{ $t('registration') }}</h2>
|
||||
<div class="panel-body">
|
||||
<div>
|
||||
<ul class="list-inline">
|
||||
<li
|
||||
v-for="network in SOCIAL_AUTH_NETWORKS"
|
||||
:key="network.key"
|
||||
>
|
||||
<button
|
||||
v-if="!user.auth[network.key].id && network.key !== 'facebook'"
|
||||
class="btn btn-primary mb-2"
|
||||
@click="socialAuth(network.key, user)"
|
||||
>
|
||||
{{ $t('registerWithSocial', {network: network.name}) }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!hasBackupAuthOption(network.key) && user.auth[network.key].id"
|
||||
class="btn btn-primary mb-2"
|
||||
disabled="disabled"
|
||||
>
|
||||
{{ $t('registeredWithSocial', {network: network.name}) }}
|
||||
</button>
|
||||
<button
|
||||
v-if="hasBackupAuthOption(network.key) && user.auth[network.key].id"
|
||||
class="btn btn-danger"
|
||||
@click="deleteSocialAuth(network)"
|
||||
>
|
||||
{{ $t('detachSocial', {network: network.name}) }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div v-if="!user.auth.local.has_password">
|
||||
<h5 v-if="!user.auth.local.email">
|
||||
{{ $t('addLocalAuth') }}
|
||||
</h5>
|
||||
<h5 v-if="user.auth.local.email">
|
||||
{{ $t('addPasswordAuth') }}
|
||||
</h5>
|
||||
<div
|
||||
class="form"
|
||||
name="localAuth"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div
|
||||
v-if="!user.auth.local.email"
|
||||
class="form-group"
|
||||
>
|
||||
<input
|
||||
v-model="localAuth.email"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('email')"
|
||||
required="required"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="localAuth.password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
:placeholder="$t('password')"
|
||||
required="required"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="localAuth.confirmPassword"
|
||||
class="form-control"
|
||||
type="password"
|
||||
:placeholder="$t('confirmPass')"
|
||||
required="required"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
@click="addLocalAuth()"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usersettings">
|
||||
<h5>{{ $t('changeDisplayName') }}</h5>
|
||||
<div
|
||||
class="form"
|
||||
name="changeDisplayName"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="changeDisplayname"
|
||||
v-model="temporaryDisplayName"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('newDisplayName')"
|
||||
:class="{'is-invalid input-invalid': displayNameInvalid}"
|
||||
>
|
||||
<div
|
||||
v-if="displayNameIssues.length > 0"
|
||||
class="mb-3"
|
||||
>
|
||||
<div
|
||||
v-for="issue in displayNameIssues"
|
||||
:key="issue"
|
||||
class="input-error"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
:disabled="displayNameCannotSubmit"
|
||||
@click="changeDisplayName(temporaryDisplayName)"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
<h5>{{ $t('changeUsername') }}</h5>
|
||||
<div
|
||||
class="form"
|
||||
name="changeUsername"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div
|
||||
v-if="verifiedUsername"
|
||||
class="iconalert iconalert-success"
|
||||
>
|
||||
{{ $t('usernameVerifiedConfirmation', {'username': user.auth.local.username}) }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="iconalert iconalert-warning"
|
||||
>
|
||||
<div class="align-middle">
|
||||
<span>{{ $t('usernameNotVerified') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="changeUsername"
|
||||
v-model="usernameUpdates.username"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('newUsername')"
|
||||
:class="{'is-invalid input-invalid': usernameInvalid}"
|
||||
@blur="restoreEmptyUsername()"
|
||||
>
|
||||
<div
|
||||
v-for="issue in usernameIssues"
|
||||
:key="issue"
|
||||
class="input-error"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
<small class="form-text text-muted">{{ $t('changeUsernameDisclaimer') }}</small>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
:disabled="usernameCannotSubmit"
|
||||
@click="changeUser('username', usernameUpdates)"
|
||||
>
|
||||
{{ $t('saveAndConfirm') }}
|
||||
</button>
|
||||
</div>
|
||||
<h5 v-if="user.auth.local.has_password">
|
||||
{{ $t('changeEmail') }}
|
||||
</h5>
|
||||
<div
|
||||
v-if="user.auth.local.email"
|
||||
class="form"
|
||||
name="changeEmail"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="changeEmail"
|
||||
v-model="emailUpdates.newEmail"
|
||||
class="form-control"
|
||||
type="text"
|
||||
:placeholder="$t('newEmail')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.auth.local.has_password"
|
||||
class="form-group"
|
||||
>
|
||||
<input
|
||||
v-model="emailUpdates.password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
:placeholder="$t('password')"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
@click="changeUser('email', emailUpdates)"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
<h5 v-if="user.auth.local.has_password">
|
||||
{{ $t('changePass') }}
|
||||
</h5>
|
||||
<div
|
||||
v-if="user.auth.local.has_password"
|
||||
class="form"
|
||||
name="changePassword"
|
||||
novalidate="novalidate"
|
||||
>
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="changePassword"
|
||||
v-model="passwordUpdates.password"
|
||||
class="form-control"
|
||||
type="password"
|
||||
:placeholder="$t('oldPass')"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="passwordUpdates.newPassword"
|
||||
class="form-control"
|
||||
type="password"
|
||||
:placeholder="$t('newPass')"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="passwordUpdates.confirmPassword"
|
||||
class="form-control"
|
||||
type="password"
|
||||
:placeholder="$t('confirmPass')"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
@click="changeUser('password', passwordUpdates)"
|
||||
>
|
||||
{{ $t('submit') }}
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<div>
|
||||
<h5>{{ $t('dangerZone') }}</h5>
|
||||
<div>
|
||||
<button
|
||||
v-b-popover.hover.auto="$t('resetAccPop')"
|
||||
class="btn btn-danger mr-2 mb-2"
|
||||
popover-trigger="mouseenter"
|
||||
popover-placement="right"
|
||||
@click="openResetModal()"
|
||||
>
|
||||
{{ $t('resetAccount') }}
|
||||
</button>
|
||||
<button
|
||||
v-b-popover.hover.auto="$t('deleteAccPop')"
|
||||
class="btn btn-danger mb-2"
|
||||
popover-trigger="mouseenter"
|
||||
@click="openDeleteModal()"
|
||||
>
|
||||
{{ $t('deleteAccount') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
input {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: fit-content;
|
||||
}
|
||||
.usersettings h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.iconalert > div > span {
|
||||
line-height: 25px;
|
||||
}
|
||||
.iconalert > div:after {
|
||||
clear: both;
|
||||
content: '';
|
||||
display: table;
|
||||
}
|
||||
.input-error {
|
||||
color: $red-50;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.sleep {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import hello from 'hellojs';
|
||||
import axios from 'axios';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { mapState } from '@/libs/store';
|
||||
import restoreModal from './restoreModal';
|
||||
import resetModal from './resetModal';
|
||||
import deleteModal from './deleteModal';
|
||||
import dayStartAdjustment from './dayStartAdjustment';
|
||||
import { SUPPORTED_SOCIAL_NETWORKS } from '@/../../common/script/constants';
|
||||
import changeClass from '@/../../common/script/ops/changeClass';
|
||||
import notificationsMixin from '../../mixins/notifications';
|
||||
import sounds from '../../libs/sounds';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
|
||||
// @TODO: this needs our window.env fix
|
||||
// import { availableLanguages } from '../../../server/libs/i18n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
restoreModal,
|
||||
resetModal,
|
||||
deleteModal,
|
||||
dayStartAdjustment,
|
||||
},
|
||||
mixins: [notificationsMixin],
|
||||
data () {
|
||||
return {
|
||||
SOCIAL_AUTH_NETWORKS: [],
|
||||
party: {},
|
||||
// Made available by the server as a script
|
||||
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
|
||||
temporaryDisplayName: '',
|
||||
usernameUpdates: { username: '' },
|
||||
emailUpdates: {},
|
||||
passwordUpdates: {},
|
||||
localAuth: {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
displayNameIssues: [],
|
||||
usernameIssues: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
availableLanguages: 'i18n.availableLanguages',
|
||||
content: 'content',
|
||||
}),
|
||||
availableAudioThemes () {
|
||||
return ['off', ...this.content.audioThemes];
|
||||
},
|
||||
hasClass () {
|
||||
return this.$store.getters['members:hasClass'](this.user);
|
||||
},
|
||||
verifiedUsername () {
|
||||
return this.user.flags.verifiedUsername;
|
||||
},
|
||||
displayNameInvalid () {
|
||||
if (this.temporaryDisplayName.length <= 1) return false;
|
||||
return !this.displayNameValid;
|
||||
},
|
||||
displayNameValid () {
|
||||
if (this.temporaryDisplayName.length <= 1) return false;
|
||||
return this.displayNameIssues.length === 0;
|
||||
},
|
||||
displayNameCannotSubmit () {
|
||||
if (this.temporaryDisplayName.length <= 1) return true;
|
||||
return !this.displayNameValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.usernameUpdates.username.length <= 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.usernameUpdates.username.length <= 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
usernameCannotSubmit () {
|
||||
if (this.usernameUpdates.username.length <= 1) return true;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
usernameUpdates: {
|
||||
handler () {
|
||||
this.validateUsername(this.usernameUpdates.username);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
temporaryDisplayName: {
|
||||
handler () {
|
||||
this.validateDisplayName(this.temporaryDisplayName);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.SOCIAL_AUTH_NETWORKS = SUPPORTED_SOCIAL_NETWORKS;
|
||||
// @TODO: We may need to request the party here
|
||||
this.party = this.$store.state.party;
|
||||
this.usernameUpdates.username = this.user.auth.local.username || null;
|
||||
this.temporaryDisplayName = this.user.profile.name;
|
||||
this.emailUpdates.newEmail = this.user.auth.local.email || null;
|
||||
this.localAuth.username = this.user.auth.local.username || null;
|
||||
this.soundIndex = 0;
|
||||
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('settings'),
|
||||
});
|
||||
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line no-process-env
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line no-process-env
|
||||
}, {
|
||||
redirect_uri: '', // eslint-disable-line
|
||||
});
|
||||
|
||||
const focusID = this.$route.query.focus;
|
||||
if (focusID !== undefined && focusID !== null) {
|
||||
this.$nextTick(() => {
|
||||
const element = document.getElementById(focusID);
|
||||
if (element !== undefined && element !== null) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSleep () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
validateDisplayName: debounce(function checkName (displayName) {
|
||||
if (displayName.length <= 1 || displayName === this.user.profile.name) {
|
||||
this.displayNameIssues = [];
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyDisplayName', {
|
||||
displayName,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.displayNameIssues = res.issues;
|
||||
} else {
|
||||
this.displayNameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
validateUsername: debounce(function checkName (username) {
|
||||
if (username.length <= 1 || username === this.user.auth.local.username) {
|
||||
this.usernameIssues = [];
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username,
|
||||
}).then(res => {
|
||||
if (res.issues !== undefined) {
|
||||
this.usernameIssues = res.issues;
|
||||
} else {
|
||||
this.usernameIssues = [];
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
set (preferenceType, subtype) {
|
||||
const settings = {};
|
||||
if (!subtype) {
|
||||
settings[`preferences.${preferenceType}`] = this.user.preferences[preferenceType];
|
||||
} else {
|
||||
settings[`preferences.${preferenceType}.${subtype}`] = this.user.preferences[preferenceType][subtype];
|
||||
}
|
||||
return this.$store.dispatch('user:set', settings);
|
||||
},
|
||||
hideHeader () {
|
||||
this.set('hideHeader');
|
||||
if (!this.user.preferences.hideHeader || !this.user.preferences.stickyHeader) return;
|
||||
this.user.preferences.hideHeader = false;
|
||||
this.set('stickyHeader');
|
||||
},
|
||||
toggleStickyHeader () {
|
||||
this.set('stickyHeader');
|
||||
},
|
||||
showTour () {
|
||||
// @TODO: Do we still use this?
|
||||
// User.set({'flags.showTour':true});
|
||||
// Guide.goto('intro', 0, true);
|
||||
},
|
||||
showBailey () {
|
||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||
},
|
||||
hasBackupAuthOption (networkKeyToCheck) {
|
||||
if (this.user.auth.local.username) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.SOCIAL_AUTH_NETWORKS.find(network => {
|
||||
if (network.key !== networkKeyToCheck) {
|
||||
if (this.user.auth[network.key]) {
|
||||
return !!this.user.auth[network.key].id;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
async changeLanguage (e) {
|
||||
const newLang = e.target.value;
|
||||
this.user.preferences.language = newLang;
|
||||
await this.set('language');
|
||||
setTimeout(() => window.location.reload(true));
|
||||
},
|
||||
async changeUser (attribute, updates) {
|
||||
await axios.put(`/api/v4/user/auth/update-${attribute}`, updates);
|
||||
if (attribute === 'username') {
|
||||
this.user.auth.local.username = updates[attribute];
|
||||
this.localAuth.username = this.user.auth.local.username;
|
||||
this.user.flags.verifiedUsername = true;
|
||||
} else if (attribute === 'email') {
|
||||
this.user.auth.local.email = updates.newEmail;
|
||||
window.alert(this.$t('emailSuccess')); // eslint-disable-line no-alert
|
||||
} else if (attribute === 'password') {
|
||||
this.passwordUpdates = {};
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: 'Habitica',
|
||||
text: this.$t('passwordSuccess'),
|
||||
type: 'success',
|
||||
timeout: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
async changeDisplayName (newName) {
|
||||
await axios.put('/api/v4/user/', { 'profile.name': newName });
|
||||
window.alert(this.$t('displayNameSuccess')); // eslint-disable-line no-alert
|
||||
this.user.profile.name = newName;
|
||||
this.temporaryDisplayName = newName;
|
||||
},
|
||||
openRestoreModal () {
|
||||
this.$root.$emit('bv::show::modal', 'restore');
|
||||
},
|
||||
openResetModal () {
|
||||
this.$root.$emit('bv::show::modal', 'reset');
|
||||
},
|
||||
openDeleteModal () {
|
||||
this.$root.$emit('bv::show::modal', 'delete');
|
||||
},
|
||||
async deleteSocialAuth (network) {
|
||||
await axios.delete(`/api/v4/user/auth/social/${network.key}`);
|
||||
this.user.auth[network.key] = {};
|
||||
this.text(this.$t('detachedSocial', { network: network.name }));
|
||||
},
|
||||
async socialAuth (network) {
|
||||
if (network === 'apple') {
|
||||
window.location.href = buildAppleAuthUrl();
|
||||
} else {
|
||||
const auth = await hello(network).login({ scope: 'email' });
|
||||
await this.$store.dispatch('auth:socialAuth', {
|
||||
auth,
|
||||
});
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
async changeClassForUser (confirmationNeeded) {
|
||||
if (confirmationNeeded && !window.confirm(this.$t('changeClassConfirmCost'))) return; // eslint-disable-line no-alert
|
||||
try {
|
||||
changeClass(this.user);
|
||||
await axios.post('/api/v4/user/change-class');
|
||||
} catch (e) {
|
||||
window.alert(e.message); // eslint-disable-line no-alert
|
||||
}
|
||||
},
|
||||
async addLocalAuth () {
|
||||
if (this.localAuth.email === '') {
|
||||
this.localAuth.email = this.user.auth.local.email;
|
||||
}
|
||||
await axios.post('/api/v4/user/auth/local/register', this.localAuth);
|
||||
window.location.href = '/user/settings/site';
|
||||
},
|
||||
restoreEmptyUsername () {
|
||||
if (this.usernameUpdates.username.length < 1) {
|
||||
this.usernameUpdates.username = this.user.auth.local.username;
|
||||
}
|
||||
},
|
||||
changeAudioTheme () {
|
||||
this.soundIndex = 0;
|
||||
this.set('sound');
|
||||
},
|
||||
playAudio () {
|
||||
this.$root.$emit('playSound', sounds[this.soundIndex]);
|
||||
this.soundIndex = (this.soundIndex + 1) % sounds.length;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -115,8 +115,6 @@
|
||||
}
|
||||
|
||||
.input-error {
|
||||
color: $red-50;
|
||||
font-size: 90%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
<button
|
||||
v-if="getPriceClass() === 'gems'
|
||||
&& !enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
|
||||
class="btn btn-primary"
|
||||
class="btn btn-primary mb-3"
|
||||
@click="purchaseGems()"
|
||||
>
|
||||
{{ $t('purchaseGems') }}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:price="item.value"
|
||||
:item-content-class="'shop_'+item.key"
|
||||
:item-content-class="`shop_${item.key}`"
|
||||
:empty-item="false"
|
||||
:popover-position="'top'"
|
||||
@click="featuredItemSelected(item)"
|
||||
|
||||
54
website/client/src/components/shops/gemPrice.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div
|
||||
v-once
|
||||
class="gem-price-div"
|
||||
:class="{'background': withBackground}"
|
||||
>
|
||||
<div
|
||||
:class="`mr-2 svg-icon gem icon-${iconSize}`"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span class="gem-price">{{ gemPrice }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gemIcon from '@/assets/svg/gem.svg';
|
||||
|
||||
export default {
|
||||
name: 'GemPrice',
|
||||
props: ['gemPrice', 'iconSize', 'withBackground'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gem: gemIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.gem-price {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
|
||||
color: $green-10;
|
||||
}
|
||||
|
||||
.gem-price-div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.background {
|
||||
align-self: center;
|
||||
border-radius: 20px;
|
||||
padding: 6px 20px;
|
||||
|
||||
background-color: rgba($green-100, 0.15);
|
||||
}
|
||||
</style>
|
||||
@@ -265,25 +265,24 @@
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.how-many-to-sell {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.number-increment {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
font-weight: bold;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 16px;
|
||||
|
||||
&.gold {
|
||||
color: $yellow-5;
|
||||
.how-many-to-sell {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.number-increment {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
font-weight: bold;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 16px;
|
||||
|
||||
&.gold {
|
||||
color: $yellow-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.total-text {
|
||||
color: $gray-50;
|
||||
@@ -291,32 +290,31 @@
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
|
||||
&.gold {
|
||||
color: $yellow-5;
|
||||
}
|
||||
}
|
||||
|
||||
button.btn.btn-primary {
|
||||
margin-top: 16px;
|
||||
padding: 4px 16px;
|
||||
height: 32px;
|
||||
|
||||
&:focus {
|
||||
border: 2px solid black;
|
||||
&.gold {
|
||||
color: $yellow-5;
|
||||
}
|
||||
}
|
||||
|
||||
.balance {
|
||||
width: 74px;
|
||||
height: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1.33;
|
||||
color: $gray-200;
|
||||
button.btn.btn-primary {
|
||||
margin-top: 16px;
|
||||
padding: 4px 16px;
|
||||
height: 32px;
|
||||
|
||||
&:focus {
|
||||
border: 2px solid black;
|
||||
}
|
||||
|
||||
.balance {
|
||||
width: 74px;
|
||||
height: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1.33;
|
||||
color: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
:item="item"
|
||||
:price="item.goldValue ? item.goldValue : item.value"
|
||||
:price-type="item.goldValue ? 'gold' : 'gem'"
|
||||
:item-content-class="'inventory_quest_scroll_'+item.key"
|
||||
:item-content-class="`inventory_quest_scroll_${item.key}`"
|
||||
:empty-item="false"
|
||||
:popover-position="'top'"
|
||||
@click="selectItem(item)"
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
<item-with-label
|
||||
v-for="drop in getDropsList(quest.drop.items, false)"
|
||||
:key="drop.type+'_'+drop.key"
|
||||
:key="`${drop.type}_${drop.key}`"
|
||||
:item="{}"
|
||||
class="item-with-label"
|
||||
>
|
||||
|
||||
@@ -346,6 +346,7 @@ import _sortBy from 'lodash/sortBy';
|
||||
import _throttle from 'lodash/throttle';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _reverse from 'lodash/reverse';
|
||||
import _find from 'lodash/find';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import Checkbox from '@/components/ui/checkbox';
|
||||
@@ -413,7 +414,7 @@ export default {
|
||||
|
||||
hidePinned: false,
|
||||
featuredGearBought: false,
|
||||
|
||||
currentEvent: null,
|
||||
backgroundUpdate: new Date(),
|
||||
};
|
||||
},
|
||||
@@ -422,7 +423,7 @@ export default {
|
||||
content: 'content',
|
||||
user: 'user.data',
|
||||
userStats: 'user.data.stats',
|
||||
currentEvent: 'worldState.data.currentEvent',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
|
||||
usersOfficalPinnedItems () {
|
||||
@@ -518,6 +519,7 @@ export default {
|
||||
});
|
||||
|
||||
this.triggerGetWorldState();
|
||||
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
||||
@@ -273,6 +273,7 @@ import _sortBy from 'lodash/sortBy';
|
||||
import _throttle from 'lodash/throttle';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _map from 'lodash/map';
|
||||
import _find from 'lodash/find';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import ShopItem from '../shopItem';
|
||||
@@ -330,6 +331,8 @@ export default {
|
||||
hidePinned: false,
|
||||
|
||||
backgroundUpdate: new Date(),
|
||||
|
||||
currentEvent: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -339,7 +342,7 @@ export default {
|
||||
user: 'user.data',
|
||||
userStats: 'user.data.stats',
|
||||
userItems: 'user.data.items',
|
||||
currentEvent: 'worldState.data.currentEvent',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
|
||||
closed () {
|
||||
@@ -422,6 +425,7 @@ export default {
|
||||
this.$root.$emit('buyModal::hidden', this.selectedItemToBuy.key);
|
||||
}
|
||||
});
|
||||
this.currentEvent = _find(this.currentEventList, event => Boolean(['winter', 'spring', 'summer', 'fall'].includes(event.season)));
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('buyModal::boughtItem');
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="container-fluid text-center">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-3">
|
||||
<h1>{{ $t('checkOutMobileApps') }}</h1>
|
||||
<div
|
||||
class="promo_habitica"
|
||||
style="border-radius:25px;margin:auto;margin-bottom:30px"
|
||||
></div>
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1&utm_source=global_co&utm_medium=prtnr&utm_content=Mar2515&utm_campaign=PartBadge&pcampaignid=MKT-AC-global-none-all-co-pr-py-PartBadges-Oct1515-1"
|
||||
>
|
||||
<img
|
||||
alt="Get it on Google Play"
|
||||
src="https://play.google.com/intl/en_us/badges/images/apps/en-play-badge.png"
|
||||
style="width:139px;height:45px;image-rendering:auto;vertical-align:top"
|
||||
>
|
||||
</a>
|
||||
<a
|
||||
href="https://geo.itunes.apple.com/us/app/habitica/id994882113?mt=8"
|
||||
style="display:inline-block;overflow:hidden;background:url(https://linkmaker.itunes.apple.com/images/badges/en-us/badge_appstore-lrg.svg#svgView) no-repeat;background-size:100%;width:152px;height:45px;margin-left:20px;image-rendering:auto"
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -880,8 +880,6 @@ export default {
|
||||
},
|
||||
mounted () {
|
||||
hello.init({
|
||||
facebook: process.env.FACEBOOK_KEY, // eslint-disable-line
|
||||
// windows: WINDOWS_CLIENT_ID,
|
||||
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
|
||||
});
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
|
||||
@@ -682,7 +682,9 @@ export default {
|
||||
// as default filter for daily
|
||||
// and set the filter as 'due' only when the component first
|
||||
// loads and not on subsequent reloads.
|
||||
if (type === 'daily' && filter === '' && !this.challenge) {
|
||||
if (
|
||||
type === 'daily' && filter === '' && !this.challenge
|
||||
) {
|
||||
filter = 'due'; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
</span>
|
||||
<label
|
||||
v-once
|
||||
class="mb-1"
|
||||
v-html="text"
|
||||
></label>
|
||||
</div>
|
||||
@@ -23,11 +22,11 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
label {
|
||||
height: 1.5rem;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
letter-spacing: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.gray-200 {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div>
|
||||
<select-list
|
||||
:items="items"
|
||||
:key-prop="'icon'"
|
||||
class="difficulty-select"
|
||||
:class="{disabled: disabled}"
|
||||
:disabled="disabled"
|
||||
@@ -10,7 +9,7 @@
|
||||
:hide-icon="true"
|
||||
@select="$emit('select', $event.value)"
|
||||
>
|
||||
<template v-slot:item="{ item, button }">
|
||||
<template #item="{ item, button }">
|
||||
<div
|
||||
v-if="item"
|
||||
class="difficulty-item"
|
||||
|
||||
@@ -203,16 +203,15 @@
|
||||
<template
|
||||
v-if="task.type !== 'reward'"
|
||||
>
|
||||
<div class="d-flex mt-3">
|
||||
<div class="d-flex mt-3 align-items-center">
|
||||
<lockable-label
|
||||
:locked="challengeAccessRequired"
|
||||
:text="$t('difficulty')"
|
||||
/>
|
||||
<div
|
||||
v-b-tooltip.hover.righttop="$t('difficultyHelp')"
|
||||
class="svg-icon info-icon mb-auto ml-1"
|
||||
v-html="icons.information"
|
||||
></div>
|
||||
<information-icon
|
||||
tooltip-id="difficultyHelp"
|
||||
:tooltip="$t('difficultyHelp')"
|
||||
/>
|
||||
</div>
|
||||
<select-difficulty
|
||||
:value="task.priority"
|
||||
@@ -452,7 +451,7 @@
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-if="task.type === 'daily' && isUserTask && purpose === 'edit'"
|
||||
v-if="advancedSettingsShowRestoreStreak"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -479,8 +478,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="task.type === 'habit'
|
||||
&& isUserTask && purpose === 'edit' && (task.up || task.down)"
|
||||
v-if="advancedSettingsShowAdjustCounter"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
@@ -539,6 +537,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="advancedSettingsShowTaskAlias"
|
||||
class="option mt-3"
|
||||
>
|
||||
<div class="form-group">
|
||||
<label
|
||||
v-once
|
||||
class="mb-1"
|
||||
>{{ $t('taskAlias') }}
|
||||
|
||||
<information-icon
|
||||
tooltip-id="taskAlias"
|
||||
:tooltip="$t('taskAliasPopover')"
|
||||
/>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="task.alias"
|
||||
class="form-control"
|
||||
:placeholder="$t('taskAliasPlaceholder')"
|
||||
type="text"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-collapse>
|
||||
</div>
|
||||
@@ -882,6 +905,11 @@
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.habit-option {
|
||||
&-container {
|
||||
min-width: 3rem;
|
||||
@@ -997,7 +1025,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
|
||||
import syncTask from '../../mixins/syncTask';
|
||||
|
||||
import informationIcon from '@/assets/svg/information.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import negativeIcon from '@/assets/svg/negative.svg';
|
||||
import streakIcon from '@/assets/svg/streak.svg';
|
||||
@@ -1006,10 +1033,12 @@ import goldIcon from '@/assets/svg/gold.svg';
|
||||
import chevronIcon from '@/assets/svg/chevron.svg';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg';
|
||||
import gripIcon from '@/assets/svg/grip.svg';
|
||||
import InformationIcon from '@/components/ui/informationIcon.vue';
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InformationIcon,
|
||||
SelectMulti,
|
||||
Datepicker,
|
||||
checklist,
|
||||
@@ -1029,7 +1058,6 @@ export default {
|
||||
showAssignedSelect: false,
|
||||
newChecklistItem: null,
|
||||
icons: Object.freeze({
|
||||
information: informationIcon,
|
||||
negative: negativeIcon,
|
||||
positive: positiveIcon,
|
||||
destroy: deleteIcon,
|
||||
@@ -1064,25 +1092,25 @@ export default {
|
||||
dayMapping: 'constants.DAY_MAPPING',
|
||||
ATTRIBUTES: 'constants.ATTRIBUTES',
|
||||
}),
|
||||
advancedSettingsAvailable () {
|
||||
if (
|
||||
this.task.type === 'reward'
|
||||
|| this.task.type === 'todo'
|
||||
|| this.purpose === 'create'
|
||||
|| !this.isUserTask
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.task.type === 'habit'
|
||||
&& !this.task.up
|
||||
&& !this.task.down
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
// region advanced settings
|
||||
advancedSettingsShowAdjustCounter () {
|
||||
return this.task.type === 'habit'
|
||||
&& this.isUserTask && this.purpose === 'edit'
|
||||
&& (this.task.up || this.task.down);
|
||||
},
|
||||
advancedSettingsShowRestoreStreak () {
|
||||
return this.task.type === 'daily' && this.isUserTask
|
||||
&& this.purpose === 'edit';
|
||||
},
|
||||
advancedSettingsShowTaskAlias () {
|
||||
return this.isUserTask && this.user.preferences.developerMode;
|
||||
},
|
||||
advancedSettingsAvailable () {
|
||||
return this.advancedSettingsShowRestoreStreak
|
||||
|| this.advancedSettingsShowAdjustCounter
|
||||
|| this.advancedSettingsShowTaskAlias;
|
||||
},
|
||||
// endregion advanced settings
|
||||
checklistEnabled () {
|
||||
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
|
||||
},
|
||||
@@ -1157,7 +1185,6 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted () {
|
||||
this.showAdvancedOptions = !this.user.preferences.advancedCollapsed;
|
||||
if (this.groupId) {
|
||||
const groupResponse = await axios.get(`/api/v4/groups/${this.groupId}`);
|
||||
this.managers = Object.keys(groupResponse.data.data.managers);
|
||||
|
||||
@@ -486,6 +486,15 @@ export default {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('tasks'),
|
||||
});
|
||||
if (this.$store.state.postLoadModal) {
|
||||
const modalToLoad = this.$store.state.postLoadModal;
|
||||
if (modalToLoad.includes('profile')) {
|
||||
this.$router.push(modalToLoad);
|
||||
} else {
|
||||
this.$root.$emit('bv::show::modal', modalToLoad);
|
||||
}
|
||||
this.$store.state.postLoadModal = '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({ setUser: 'user:set' }),
|
||||
|
||||
37
website/client/src/components/ui/informationIcon.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<span class="ml-1">
|
||||
<div
|
||||
:id="`tooltip_${tooltipId}`"
|
||||
class="svg-icon icon-16"
|
||||
:title="tooltip"
|
||||
v-html="icons.information"
|
||||
></div>
|
||||
<b-tooltip
|
||||
:title="tooltip"
|
||||
:target="`tooltip_${tooltipId}`"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import informationIcon from '@/assets/svg/information.svg';
|
||||
|
||||
export default {
|
||||
name: 'InformationIcon',
|
||||
props: ['tooltipId', 'tooltip'],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
information: informationIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -9,7 +9,7 @@
|
||||
@show="isOpened = true"
|
||||
@hide="isOpened = false"
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<template #button-content>
|
||||
<slot
|
||||
name="item"
|
||||
:item="selected || placeholder"
|
||||
@@ -21,13 +21,13 @@
|
||||
</template>
|
||||
<b-dropdown-item
|
||||
v-for="item in items"
|
||||
:key="keyProp ? item[keyProp] : item"
|
||||
:disabled="typeof item[disabledProp] === 'undefined' ? false : item[disabledProp]"
|
||||
:active="item === selected"
|
||||
:key="getKeyProp(item)"
|
||||
:disabled="isDisabled(item)"
|
||||
:active="isSelected(item)"
|
||||
:class="{
|
||||
active: item === selected,
|
||||
active: isSelected(item),
|
||||
selectListItem: true,
|
||||
showIcon: !hideIcon && item === selected
|
||||
showIcon: !hideIcon && isSelected(item)
|
||||
}"
|
||||
@click="selectItem(item)"
|
||||
>
|
||||
@@ -51,39 +51,39 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.select-list ::v-deep {
|
||||
.dropdown-toggle {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 25px; /* To allow enough room for the down arrow to be displayed */
|
||||
.select-list ::v-deep {
|
||||
.dropdown-toggle {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 25px; /* To allow enough room for the down arrow to be displayed */
|
||||
}
|
||||
|
||||
.selectListItem {
|
||||
position: relative;
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.selectListItem {
|
||||
position: relative;
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
&:not(.showIcon) {
|
||||
.svg-icon.check-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.showIcon) {
|
||||
.svg-icon.check-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon.check-icon.color {
|
||||
margin-left: 10px; /* So the flex item (checkmark) will have some spacing from the text */
|
||||
width: 0.77rem;
|
||||
height: 0.615rem;
|
||||
color: $purple-300;
|
||||
}
|
||||
.svg-icon.check-icon.color {
|
||||
margin-left: 10px; /* So the flex item (checkmark) will have some spacing from the text */
|
||||
width: 0.77rem;
|
||||
height: 0.615rem;
|
||||
color: $purple-300;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -101,6 +101,9 @@ export default {
|
||||
keyProp: {
|
||||
type: String,
|
||||
},
|
||||
activeKeyProp: {
|
||||
type: String,
|
||||
},
|
||||
disabledProp: {
|
||||
type: String,
|
||||
},
|
||||
@@ -128,10 +131,23 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getKeyProp (item) {
|
||||
return this.keyProp ? item[this.keyProp] : item;
|
||||
},
|
||||
isDisabled (item) {
|
||||
return typeof item[this.disabledProp] === 'undefined' ? false : item[this.disabledProp];
|
||||
},
|
||||
selectItem (item) {
|
||||
this.selected = item;
|
||||
this.selected = this.getKeyProp(item);
|
||||
this.$emit('select', item);
|
||||
},
|
||||
isSelected (item) {
|
||||
if (this.activeKeyProp) {
|
||||
return item[this.activeKeyProp] === this.selected;
|
||||
}
|
||||
|
||||
return item === this.selected;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
131
website/client/src/components/ui/validatedTextInput.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="label-line">
|
||||
<div
|
||||
v-if="settingsLabel"
|
||||
class="settings-label"
|
||||
>
|
||||
{{ $t(settingsLabel) }}
|
||||
</div>
|
||||
|
||||
<slot name="top-right"></slot>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div
|
||||
class="input-group"
|
||||
:class="{
|
||||
'is-valid': validStyle,
|
||||
'is-invalid': invalidStyle
|
||||
}"
|
||||
>
|
||||
<input
|
||||
:value="value"
|
||||
class="form-control"
|
||||
:type="inputType"
|
||||
:class="{
|
||||
'is-invalid input-invalid': invalidStyle,
|
||||
'is-valid input-valid': validStyle
|
||||
}"
|
||||
:readonly="readonly"
|
||||
:aria-readonly="readonly"
|
||||
|
||||
:placeholder="placeholder"
|
||||
@keyup="handleChange"
|
||||
@blur="$emit('blur')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="issue in invalidIssues"
|
||||
:key="issue"
|
||||
class="input-error"
|
||||
>
|
||||
{{ issue }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ValidatedTextInput',
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'update:value',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isValid: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onlyShowInvalidState: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
inputType: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
settingsLabel: {
|
||||
type: String,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
invalidIssues: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
wasChanged: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canChangeClasses () {
|
||||
return !this.readonly && this.wasChanged;
|
||||
},
|
||||
validStyle () {
|
||||
return this.canChangeClasses && this.isValid && !this.onlyShowInvalidState;
|
||||
},
|
||||
invalidStyle () {
|
||||
return this.canChangeClasses && !this.isValid;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleChange ({ target: { value } }) {
|
||||
this.wasChanged = true;
|
||||
this.$emit('update:value', value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.label-line {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.input-error {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -9,8 +9,19 @@
|
||||
{{ displayName }}
|
||||
<div
|
||||
class="svg-icon icon-12"
|
||||
:class="{ 'margin-bump': context === 'profile' }"
|
||||
v-html="tierIcon()"
|
||||
></div>
|
||||
<div
|
||||
v-if="showBuffed"
|
||||
v-b-tooltip.hover.bottom="$t('buffed')"
|
||||
class="is-buffed ml-2 d-flex align-items-center"
|
||||
>
|
||||
<div
|
||||
class="svg-icon m-auto"
|
||||
v-html="icons.buff"
|
||||
></div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
@@ -39,7 +50,12 @@
|
||||
|
||||
&[class*="tier"] .svg-icon {
|
||||
margin-top: 5px;
|
||||
|
||||
&.margin-bump {
|
||||
margin-top: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
&.npc .svg-icon {
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -52,6 +68,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-buffed {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: $header-dark-background;
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
|
||||
.svg-icon {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 12px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -70,6 +101,7 @@ import tier7 from '@/assets/svg/tier-7.svg';
|
||||
import tier8 from '@/assets/svg/tier-mod.svg';
|
||||
import tier9 from '@/assets/svg/tier-staff.svg';
|
||||
import tierNPC from '@/assets/svg/tier-npc.svg';
|
||||
import buffIcon from '@/assets/svg/buff.svg';
|
||||
|
||||
export default {
|
||||
mixins: [styleHelper],
|
||||
@@ -81,6 +113,8 @@ export default {
|
||||
'contributor',
|
||||
'hideTooltip',
|
||||
'smallerStyle',
|
||||
'showBuffed',
|
||||
'context',
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
@@ -95,6 +129,7 @@ export default {
|
||||
tier8,
|
||||
tier9,
|
||||
tierNPC,
|
||||
buff: buffIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
263
website/client/src/components/userMenu/profileAchievs.vue
Normal file
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div
|
||||
id="achievements"
|
||||
class="standard-page"
|
||||
>
|
||||
<div
|
||||
v-for="(category, key) in achievements"
|
||||
:key="key"
|
||||
class="row category-row"
|
||||
>
|
||||
<h3 class="text-center">
|
||||
{{ $t(`${key}Achievs`) }}
|
||||
</h3>
|
||||
<div class="">
|
||||
<div class="row achievements-row justify-content-center">
|
||||
<div
|
||||
v-for="(achievement, achievKey) in achievementsCategory(key, category)"
|
||||
:key="achievKey"
|
||||
class="achievement-wrapper col text-center"
|
||||
>
|
||||
<div
|
||||
:id="achievKey + '-achievement'"
|
||||
class="box achievement-container d-flex justify-content-center align-items-middle"
|
||||
:class="{'achievement-unearned': !achievement.earned}"
|
||||
>
|
||||
<b-popover
|
||||
:target="'#' + achievKey + '-achievement'"
|
||||
triggers="hover"
|
||||
placement="top"
|
||||
>
|
||||
<h4 class="popover-content-title">
|
||||
{{ achievement.title }}
|
||||
</h4>
|
||||
<div
|
||||
class="popover-content-text"
|
||||
v-html="achievement.text"
|
||||
></div>
|
||||
</b-popover>
|
||||
<div
|
||||
v-if="achievement.earned"
|
||||
class="achievement m-auto"
|
||||
:class="achievement.icon + '2x'"
|
||||
>
|
||||
<div
|
||||
v-if="achievement.optionalCount"
|
||||
class="counter badge badge-pill stack-count"
|
||||
>
|
||||
{{ achievement.optionalCount }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!achievement.earned"
|
||||
class="achievement achievement-unearned achievement-unearned2x"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="achievementsCategories[key].number > 5"
|
||||
class="btn btn-flat btn-show-more"
|
||||
@click="toggleAchievementsCategory(key)"
|
||||
>
|
||||
{{ achievementsCategories[key].open ?
|
||||
$t('hideAchievements', {category: $t(`${key}Achievs`)}) :
|
||||
$t('showAllAchievements', {category: $t(`${key}Achievs`)})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="">
|
||||
<div class="row">
|
||||
<div
|
||||
v-if="user.achievements.challenges"
|
||||
class="col-12 col-md-6"
|
||||
>
|
||||
<div class="achievement-icon achievement-karaoke-2x"></div>
|
||||
<h3 class="text-center">
|
||||
{{ $t('challengesWon') }}
|
||||
</h3>
|
||||
<div
|
||||
v-for="chal in user.achievements.challenges"
|
||||
:key="chal"
|
||||
class="achievement-list-item"
|
||||
>
|
||||
<span v-markdown="chal"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.achievements.quests"
|
||||
class="col-12 col-md-6"
|
||||
>
|
||||
<div class="achievement-icon achievement-alien2x"></div>
|
||||
<h3 class="text-center">
|
||||
{{ $t('questsCompleted') }}
|
||||
</h3>
|
||||
<div
|
||||
v-for="(value, key) in user.achievements.quests"
|
||||
:key="key"
|
||||
class="achievement-list-item d-flex justify-content-between"
|
||||
>
|
||||
<span>{{ content.quests[key].text() }}</span>
|
||||
<span
|
||||
v-if="value > 1"
|
||||
class="badge badge-pill stack-count"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#achievements {
|
||||
.category-row {
|
||||
margin-bottom: 34px;
|
||||
justify-content: center;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.achievements-row {
|
||||
margin: 0 auto;
|
||||
max-width: 590px;
|
||||
}
|
||||
|
||||
.achievement-wrapper {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
max-width: 94px;
|
||||
min-width: 94px;
|
||||
padding: 0px;
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
.box {
|
||||
background: $white;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 48px;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.box.achievement-unearned {
|
||||
background-color: $gray-600;
|
||||
}
|
||||
|
||||
.counter.badge {
|
||||
background-color: $orange-100;
|
||||
color: $white;
|
||||
max-height: 24px;
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: -12.8px;
|
||||
}
|
||||
|
||||
.achievement-icon {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.achievement-list-item {
|
||||
border-top: 1px solid $gray-500;
|
||||
padding-bottom: 12px;
|
||||
padding-top: 11px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: $gray-600;
|
||||
color: $gray-300;
|
||||
height: fit-content;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// import moment from 'moment';
|
||||
// import axios from 'axios';
|
||||
// import each from 'lodash/each';
|
||||
// import cloneDeep from 'lodash/cloneDeep';
|
||||
// import closeX from '../ui/closeX';
|
||||
|
||||
import achievementsLib from '@/../../common/script/libs/achievements';
|
||||
import Content from '@/../../common/script/content';
|
||||
import error404 from '../404';
|
||||
// import { userCustomStateMixin } from '../../mixins/userState';
|
||||
|
||||
export default {
|
||||
components:
|
||||
error404,
|
||||
// closeX,
|
||||
props: ['userId', 'startingPage'],
|
||||
data () {
|
||||
return {
|
||||
selectedPage: 'achievements',
|
||||
achievements: {},
|
||||
achievementsCategories: {}, // number, open
|
||||
content: Content,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async loadUser () {
|
||||
let user = null;
|
||||
|
||||
const profileUserId = this.userId;
|
||||
|
||||
if (profileUserId && profileUserId !== this.userLoggedIn._id) {
|
||||
const response = await this.$store.dispatch('members:fetchMember', {
|
||||
memberId: profileUserId,
|
||||
unpack: false,
|
||||
});
|
||||
if (response.response && response.response.status === 404) {
|
||||
user = null;
|
||||
this.$store.dispatch('snackbars:add', {
|
||||
title: 'Habitica',
|
||||
text: this.$t('messageDeletedUser'),
|
||||
type: 'error',
|
||||
timeout: false,
|
||||
});
|
||||
} else if (response.status && response.status === 200) {
|
||||
user = response.data.data;
|
||||
}
|
||||
} else {
|
||||
user = this.userLoggedIn;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
if (!user.achievements.quests) user.achievements.quests = {};
|
||||
if (!user.achievements.challenges) user.achievements.challenges = {};
|
||||
// @TODO: this common code should handle the above
|
||||
this.achievements = achievementsLib.getAchievementsForProfile(user);
|
||||
|
||||
const achievementsCategories = {};
|
||||
Object.keys(this.achievements).forEach(category => {
|
||||
achievementsCategories[category] = {
|
||||
open: false,
|
||||
number: Object.keys(this.achievements[category].achievements).length,
|
||||
};
|
||||
});
|
||||
|
||||
this.achievementsCategories = achievementsCategories;
|
||||
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
this.userLoaded = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -1,12 +1,16 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="profile"
|
||||
size="lg"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
@hide="beforeHide"
|
||||
@shown="onShown()"
|
||||
>
|
||||
<div slot="modal-header">
|
||||
<close-x
|
||||
@close="close()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<profile
|
||||
:user-id="userId"
|
||||
:starting-page="startingPage"
|
||||
@@ -15,33 +19,62 @@
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#profile {
|
||||
.modal-header {
|
||||
background-color: $white;
|
||||
border-bottom: none;
|
||||
padding: 0px;
|
||||
}
|
||||
.modal-dialog {
|
||||
max-width: 684px;
|
||||
}
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
background-color: $white;
|
||||
}
|
||||
.modal-content {
|
||||
background: $gray-700;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
.modal-close {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import profile from './profile';
|
||||
import closeX from '../ui/closeX';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
profile,
|
||||
closeX,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
userId: undefined,
|
||||
startingPage: undefined,
|
||||
path: undefined,
|
||||
fromPath: undefined,
|
||||
toPath: undefined,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:show-profile', data => {
|
||||
this.userId = data.userId;
|
||||
this.startingPage = data.startingPage || 'profile';
|
||||
this.path = data.path;
|
||||
this.fromPath = data.fromPath;
|
||||
this.toPath = data.toPath;
|
||||
this.$root.$emit('bv::show::modal', 'profile');
|
||||
});
|
||||
},
|
||||
@@ -50,13 +83,16 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onShown () {
|
||||
window.history.pushState('', null, this.path);
|
||||
window.history.pushState('', null, this.toPath);
|
||||
},
|
||||
beforeHide () {
|
||||
if (this.$route.path !== window.location.pathname) {
|
||||
this.$router.back();
|
||||
window.history.pushState('', null, this.fromPath);
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'profile');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="standard-page">
|
||||
<profile
|
||||
:user-id="userId"
|
||||
:starting-page="startingPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import profile from './profile';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
profile,
|
||||
},
|
||||
props: {
|
||||
userId: String,
|
||||
startingPage: {
|
||||
type: String,
|
||||
default: 'profile',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -120,27 +120,27 @@
|
||||
</h2>
|
||||
<div class="well pet-mount-well">
|
||||
<div class="pet-mount-well-image">
|
||||
<div
|
||||
class="box"
|
||||
:class="{white: user.items.currentPet}"
|
||||
>
|
||||
<div
|
||||
class="box"
|
||||
:class="{white: user.items.currentPet}"
|
||||
>
|
||||
<div
|
||||
class="Pet"
|
||||
:class="`Pet-${user.items.currentPet}`"
|
||||
></div>
|
||||
</div>
|
||||
class="Pet"
|
||||
:class="`Pet-${user.items.currentPet}`"
|
||||
></div>
|
||||
</div>
|
||||
<div class="pet-mount-well-text">
|
||||
<div>{{ formatAnimal(user.items.currentPet, 'pet') }}</div>
|
||||
<div>
|
||||
<strong>{{ $t('petsFound') }}:</strong>
|
||||
{{ totalCount(user.items.pets) }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $t('beastMasterProgress') }}:</strong>
|
||||
{{ beastMasterProgress(user.items.pets) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pet-mount-well-text">
|
||||
<div>{{ formatAnimal(user.items.currentPet, 'pet') }}</div>
|
||||
<div>
|
||||
<strong>{{ $t('petsFound') }}:</strong>
|
||||
{{ totalCount(user.items.pets) }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $t('beastMasterProgress') }}:</strong>
|
||||
{{ beastMasterProgress(user.items.pets) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-section-mounts col-12 col-md-6">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import BootstrapVue from 'bootstrap-vue';
|
||||
import Fragment from 'vue-fragment';
|
||||
import AppComponent from './app';
|
||||
import {
|
||||
setup as setupAnalytics,
|
||||
@@ -28,6 +29,7 @@ Vue.config.productionTip = IS_PRODUCTION;
|
||||
Vue.use(i18n, { i18nData: window && window['habitica-i18n'] });
|
||||
Vue.use(StoreModule);
|
||||
Vue.use(BootstrapVue);
|
||||
Vue.use(Fragment.Plugin);
|
||||
|
||||
setUpLogging();
|
||||
setupAnalytics(); // just create queues for analytics, no scripts loaded at this time
|
||||
|
||||
23
website/client/src/mixins/copyToClipboard.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import notifications from './notifications';
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
methods: {
|
||||
async mixinCopyToClipboard (valueToCopy, notificationToShow = null) {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(valueToCopy);
|
||||
} else {
|
||||
// fallback if clipboard API does not exist
|
||||
const copyText = document.createElement('textarea');
|
||||
copyText.value = valueToCopy;
|
||||
document.body.appendChild(copyText);
|
||||
copyText.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(copyText);
|
||||
}
|
||||
if (notificationToShow) {
|
||||
this.text(notificationToShow);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -10,7 +10,7 @@ function toFixedWithoutRounding (num, fixed) {
|
||||
return num.toString().match(re)[0];
|
||||
}
|
||||
|
||||
export default {
|
||||
export const NotificationMixins = {
|
||||
computed: {
|
||||
...mapState({ notifications: 'notificationStore' }),
|
||||
},
|
||||
@@ -33,6 +33,7 @@ export default {
|
||||
this.notify(`${this.sign(val)}${val}`, 'damage');
|
||||
},
|
||||
exp (val) {
|
||||
if (val === 0) return;
|
||||
const message = getXPMessage(val);
|
||||
this.notify(message, 'xp', 'glyphicon glyphicon-star', this.sign(val));
|
||||
},
|
||||
@@ -90,3 +91,5 @@ export default {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default NotificationMixins;
|
||||
|
||||
62
website/client/src/mixins/passwordInputChecks.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Component Example
|
||||
*
|
||||
* <current-password-input
|
||||
* :show-forget-password="true"
|
||||
* :is-valid="mixinData.currentPasswordIssues.length === 0"
|
||||
* :invalid-issues="mixinData.currentPasswordIssues"
|
||||
* @passwordValue="updates.password = $event"
|
||||
* />
|
||||
*/
|
||||
|
||||
export const PasswordInputChecksMixin = {
|
||||
data () {
|
||||
return {
|
||||
mixinData: {
|
||||
currentPasswordIssues: [],
|
||||
newPasswordIssues: [],
|
||||
confirmPasswordIssues: [],
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clearPasswordIssues () {
|
||||
this.mixinData.currentPasswordIssues.length = 0;
|
||||
this.mixinData.newPasswordIssues.length = 0;
|
||||
this.mixinData.confirmPasswordIssues.length = 0;
|
||||
},
|
||||
/**
|
||||
* @param {() => Promise<void>} promiseCall
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async passwordInputCheckMixinTryCall (promiseCall) {
|
||||
try {
|
||||
// reset previous issues
|
||||
this.clearPasswordIssues();
|
||||
|
||||
await promiseCall();
|
||||
} catch (axiosError) {
|
||||
const message = axiosError.response?.data?.message;
|
||||
|
||||
if ([this.$t('wrongPassword'), this.$t('missingPassword')].includes(message)) {
|
||||
this.mixinData.currentPasswordIssues.push(message);
|
||||
} else if ([this.$t('missingNewPassword'), this.$t('passwordIssueLength'), this.$t('passwordConfirmationMatch')].includes(message)) {
|
||||
this.mixinData.newPasswordIssues.push(message);
|
||||
this.mixinData.confirmPasswordIssues.push(message);
|
||||
} else if (this.$t('invalidReqParams') === message) {
|
||||
const errors = axiosError.response?.data?.errors ?? [];
|
||||
|
||||
for (const error of errors) {
|
||||
if (error.param === 'password') {
|
||||
this.mixinData.currentPasswordIssues.push(error.message);
|
||||
} else if (error.param === 'newPassword') {
|
||||
this.mixinData.newPasswordIssues.push(error.message);
|
||||
} else {
|
||||
this.mixinData.confirmPasswordIssues.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { mapState } from '@/libs/store';
|
||||
export const userCustomStateMixin = fieldname => {
|
||||
const map = { };
|
||||
map[fieldname] = 'user.data';
|
||||
return { // eslint-disable-line import/prefer-default-export
|
||||
return {
|
||||
computed: {
|
||||
...mapState(map),
|
||||
},
|
||||
|
||||
238
website/client/src/pages/settings-overview.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<secondary-menu class="col-12">
|
||||
<template
|
||||
v-for="routePath in tabs"
|
||||
>
|
||||
<router-link
|
||||
v-if="allowedToShowTab(routePath)"
|
||||
:key="routePath"
|
||||
|
||||
class="nav-link"
|
||||
:to="{name: routePath}"
|
||||
exact="exact"
|
||||
:class="{'active': $route.name === routePath}"
|
||||
>
|
||||
{{ $t(pathTranslateKey(routePath)) }}
|
||||
</router-link>
|
||||
</template>
|
||||
</secondary-menu>
|
||||
<div
|
||||
v-if="$route.name === 'subscription' && promo === 'g1g1'"
|
||||
class="g1g1-banner d-flex justify-content-center"
|
||||
@click="showSelectUser"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-gifts left-gift"
|
||||
v-html="icons.gifts"
|
||||
>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center text-center">
|
||||
<strong
|
||||
class="mt-auto mb-1"
|
||||
> {{ $t('g1g1Event') }} </strong>
|
||||
<p
|
||||
class="mb-auto"
|
||||
>
|
||||
{{ $t('g1g1Details') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-gifts right-gift"
|
||||
v-html="icons.gifts"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-12 d-flex "
|
||||
:class="{'justify-content-center': applyNarrowView}"
|
||||
>
|
||||
<div :class="{'settings-content': applyNarrowView, 'full-width-content': !applyNarrowView}">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
strong {
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.g1g1-banner {
|
||||
color: $white;
|
||||
width: 100%;
|
||||
height: 5.75rem;
|
||||
background-image: linear-gradient(90deg, $teal-50 0%, $purple-400 100%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left-gift {
|
||||
margin: auto 3rem auto auto;
|
||||
}
|
||||
|
||||
.right-gift {
|
||||
margin: auto auto auto 3rem;
|
||||
filter: flipH;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.svg-gifts {
|
||||
width: 3.5rem;
|
||||
}
|
||||
|
||||
.full-width-content {
|
||||
width: 100%;
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
flex: 0 0 732px;
|
||||
max-width: unset;
|
||||
|
||||
::v-deep {
|
||||
line-height: 1.71;
|
||||
|
||||
.small {
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
table td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
table tr.expanded td {
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-label {
|
||||
font-weight: bold;
|
||||
color: $gray-50;
|
||||
|
||||
width: 23%;
|
||||
}
|
||||
|
||||
.input-area .settings-label {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.settings-value {
|
||||
color: $gray-50;
|
||||
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
width: 30%;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: $purple-300;
|
||||
|
||||
&.danger {
|
||||
color: $maroon-50;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-disclaimer {
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
width: 320px;
|
||||
margin: 1rem auto 0;
|
||||
}
|
||||
|
||||
.edit-link {
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-link {
|
||||
color: $maroon-50 !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import { mapState } from '@/libs/store';
|
||||
import SecondaryMenu from '@/components/secondaryMenu';
|
||||
import gifts from '@/assets/svg/gifts-vertical.svg';
|
||||
import { userStateMixin } from '@/mixins/userState';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SecondaryMenu,
|
||||
},
|
||||
mixins: [userStateMixin],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
gifts,
|
||||
}),
|
||||
tabs: [
|
||||
'general',
|
||||
'subscription',
|
||||
'siteData',
|
||||
'promoCode',
|
||||
'transactions',
|
||||
'notifications',
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
currentEvent () {
|
||||
return find(this.currentEventList, event => Boolean(event.promo));
|
||||
},
|
||||
promo () {
|
||||
if (!this.currentEvent || !this.currentEvent.promo) return 'none';
|
||||
return this.currentEvent.promo;
|
||||
},
|
||||
applyNarrowView () {
|
||||
return !['subscription', 'transactions'].includes(this.$route.name);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* @param {String} tabName
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
allowedToShowTab (tabName) {
|
||||
const transactionsTab = tabName === 'transactions';
|
||||
|
||||
return transactionsTab
|
||||
? this.hasPermission(this.user, 'userSupport')
|
||||
: true;
|
||||
},
|
||||
|
||||
showSelectUser () {
|
||||
this.$root.$emit('bv::show::modal', 'select-user-modal');
|
||||
},
|
||||
pathTranslateKey (path) {
|
||||
if (path === 'api') {
|
||||
return 'API';
|
||||
}
|
||||
return path;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div
|
||||
class="class-value"
|
||||
:class="{[selectedClass]: !classDisabled, disabled: classDisabled}"
|
||||
>
|
||||
<span
|
||||
v-if="!classDisabled"
|
||||
class="svg-icon icon-16 mr-2"
|
||||
v-html="classIcons[selectedClass]"
|
||||
></span>
|
||||
|
||||
<span
|
||||
v-if="classDisabled"
|
||||
class="label"
|
||||
>
|
||||
{{ $t('noClassSelected') }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="label"
|
||||
>
|
||||
{{ $t(selectedClass) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import warriorIcon from '@/assets/svg/warrior.svg';
|
||||
import rogueIcon from '@/assets/svg/rogue.svg';
|
||||
import healerIcon from '@/assets/svg/healer.svg';
|
||||
import wizardIcon from '@/assets/svg/wizard.svg';
|
||||
|
||||
export default {
|
||||
name: 'ClassIconLabel',
|
||||
props: ['selectedClass', 'classDisabled'],
|
||||
data () {
|
||||
return {
|
||||
classIcons: Object.freeze({
|
||||
warrior: warriorIcon,
|
||||
rogue: rogueIcon,
|
||||
healer: healerIcon,
|
||||
wizard: wizardIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.class-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:not(.disabled) {
|
||||
.label {
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.healer {
|
||||
color: $healer-color;
|
||||
}
|
||||
|
||||
.rogue {
|
||||
color: $rogue-color;
|
||||
}
|
||||
|
||||
.warrior {
|
||||
color: $warrior-color;
|
||||
}
|
||||
|
||||
.wizard {
|
||||
color: $wizard-color;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
color: $maroon-50;
|
||||
}
|
||||
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="input-area">
|
||||
<validated-text-input
|
||||
v-model="currentPassword"
|
||||
:settings-label="customLabel ?? 'password'"
|
||||
|
||||
:placeholder="$t(customLabel ?? 'password')"
|
||||
:is-valid="isValid"
|
||||
:invalid-issues="invalidIssues"
|
||||
:only-show-invalid-state="true"
|
||||
input-type="password"
|
||||
@update:value="$emit('passwordValue', currentPassword)"
|
||||
>
|
||||
<div
|
||||
v-if="showForgetPassword"
|
||||
slot="top-right"
|
||||
class="forgot-password"
|
||||
>
|
||||
<router-link
|
||||
to="/forgot-password"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('forgotPassword') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</validated-text-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import ValidatedTextInput from '@/components/ui/validatedTextInput.vue';
|
||||
|
||||
export default {
|
||||
name: 'CurrentPasswordInput',
|
||||
components: { ValidatedTextInput },
|
||||
props: ['customLabel', 'showForgetPassword', 'isValid', 'invalidIssues'],
|
||||
data () {
|
||||
return {
|
||||
currentPassword: '',
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.forgot-password {
|
||||
a {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
}
|
||||
</style>
|
||||