mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-13 03:22:52 -05:00
Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e8e7318f3 | |||
| 1c3d4a6fd5 | |||
| 9ed2359c77 | |||
| 0dc21fa868 | |||
| b87527bcea | |||
| 0adfc9f756 | |||
| 9a2b49b4bf | |||
| 750a02053c | |||
| 2748455f16 | |||
| cc0e807609 | |||
| bbc5a54a3e | |||
| 8e717de039 | |||
| ce18e614be | |||
| 58c27c2610 | |||
| 220dd51f85 | |||
| 3b1407f529 | |||
| be7b3076eb | |||
| 2b4ffdf27f | |||
| e0dc608fd8 | |||
| 0b4059aab0 | |||
| 3aa7b8b447 | |||
| 94b9bb1036 | |||
| a2f169ab76 | |||
| 6d345740ff | |||
| 294cc63fef | |||
| 9a879d566e | |||
| 8ecb0f45b5 | |||
| b04df06a37 | |||
| 706cffa71d | |||
| 00b8f4fef5 | |||
| 5ea675b8a5 | |||
| 78f0c71387 | |||
| e674ef4035 | |||
| 1655e2e03a | |||
| 0d444a9d6a | |||
| de331f5e76 | |||
| c48043ec90 | |||
| ac4d148170 | |||
| ca49e995be | |||
| d47a867149 | |||
| f4d9c6271b | |||
| 0e458683fd | |||
| 76ab93f501 | |||
| 5d4600f5c7 | |||
| 13c4a726c7 | |||
| 43122805fb | |||
| 1262d8f36e | |||
| 2b1635ff62 | |||
| e1664d2f87 | |||
| 6975b6061b | |||
| 695a5cc24d | |||
| dcced2debb | |||
| 88af9c13a8 | |||
| d5926ef7f1 | |||
| e9222e4f7c | |||
| cd0278c6b3 | |||
| 9680c94087 | |||
| 76de241675 | |||
| 3ba6b4a209 | |||
| b101d43e62 | |||
| 59a1a2783c | |||
| 517fbc3c8e | |||
| c0e8d80966 | |||
| afacd497d7 | |||
| dc744de4a9 | |||
| b042d4b899 | |||
| f0c25dab05 | |||
| f44bebb573 | |||
| 8ccf701aec | |||
| 170146f91e | |||
| 239821a321 | |||
| d0dd16c797 | |||
| 8c3517caab | |||
| 4c2c1c29a3 | |||
| fb086bb654 | |||
| db6310f8ab | |||
| 08288db1ef | |||
| 4a3a7db52a | |||
| 0f8ed2c06a | |||
| 0ceb0fd844 | |||
| 285fcbd71f | |||
| 36d82a1d39 | |||
| 3dd3639964 | |||
| 5b20961908 | |||
| f4fd5e221e | |||
| 4be863de99 | |||
| 905d749e7b | |||
| 65d2eac4c3 | |||
| a0884b5d24 | |||
| 4d10c53216 | |||
| 4da6467486 | |||
| bb37adb97b | |||
| 02fef2d0d9 | |||
| d668fd8920 | |||
| 85c7c7ea57 | |||
| 0b1907fe07 | |||
| 87944c45c3 | |||
| 86068f42d4 | |||
| 2fe1ac75d2 | |||
| c0be7c77d1 | |||
| 37ff1c2c5c | |||
| 1ccd0e8f70 | |||
| 82977e893b | |||
| 84046df884 | |||
| 46cbd6cf0d | |||
| 2cb724e6b2 | |||
| d76d5f2cf1 | |||
| 2d1bf74858 | |||
| 986ba1b8ba | |||
| d68cacdc1d | |||
| c9671b5319 | |||
| 6d2db6693c | |||
| cd9e3aeab3 | |||
| 341517083e | |||
| d726b88a86 | |||
| afc7b1218a | |||
| 2c311952c8 | |||
| 85a85b7173 | |||
| 556d1e49ce | |||
| 19c1484928 | |||
| 9f135782ef | |||
| ad0907274b | |||
| 9eb959840b | |||
| 7d11b22da0 | |||
| f3495ba4e1 | |||
| dd978b664b | |||
| 84011f36b2 | |||
| b9389f8430 | |||
| 266ea01bc8 | |||
| c170edae92 | |||
| 324076438c | |||
| 77791e186f | |||
| b3a9e79f0d | |||
| ce67f06bab | |||
| 219bdc088b | |||
| ac972fb481 | |||
| 5a0813fe7f | |||
| d3673349a9 | |||
| 8125bea89f | |||
| e16c12fc7f | |||
| 90e03653ce | |||
| dd5066ab72 | |||
| ec0be8d91f | |||
| 331c64a83a | |||
| f8c6a859bf | |||
| 8040d14c28 | |||
| 8358da227d | |||
| 74b59d2324 | |||
| 5fa705c390 | |||
| 5bcfdbe066 | |||
| 0e7f98ad14 | |||
| 9ba0bd7c06 | |||
| 5d78fce468 | |||
| 31385b3e7b | |||
| 1863a965c7 | |||
| 1f2c926a54 | |||
| e7f60cb68f | |||
| d1cb86a428 | |||
| 6f6c981c92 | |||
| 63bb2b31bc | |||
| b42ac207b2 | |||
| c0e22377d1 | |||
| 5a13231027 | |||
| 4e78d72cee | |||
| 980e35880f | |||
| 5bc544c481 | |||
| 1a74d2b3b0 | |||
| d132b057eb | |||
| 761d70ec55 | |||
| a0de2dab49 | |||
| aa2458d564 | |||
| a1a3022392 | |||
| 91fdeb0e51 | |||
| 3a8fbbe394 | |||
| bc1708be14 | |||
| 75ce44e6d9 | |||
| d317bd1c41 | |||
| 1a920d6e17 | |||
| 5e12b7b042 | |||
| de4ebbac7b | |||
| ea3b27ff17 | |||
| 8c3c4c1d49 | |||
| 50577fa39b | |||
| 30c4dd86df | |||
| f9857efbac | |||
| 1a7b264958 |
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"ACCOUNT_MIN_CHAT_AGE": "0",
|
||||
"ADMIN_EMAIL": "you@example.com",
|
||||
"AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
|
||||
"AMAZON_PAYMENTS_MODE": "sandbox",
|
||||
|
||||
@@ -119,7 +119,7 @@ function path(obj, path, def) {
|
||||
* @param {String} path dot separated
|
||||
* @param {*} def default value ( if result undefined )
|
||||
* @returns {*}
|
||||
* http://stackoverflow.com/a/16190716
|
||||
* https://stackoverflow.com/a/16190716
|
||||
* Usage: console.log(path(someObject, pathname));
|
||||
*/
|
||||
for(var i = 0,path = path.split('.'),len = path.length; i < len; i++){
|
||||
|
||||
+1
-1
Submodule habitica-images updated: 08edadc432...58b8905b08
@@ -2,7 +2,7 @@
|
||||
|
||||
// TODO it might be better we just find() and save() all user objects using mongoose, and rely on our defined pre('save')
|
||||
// and default values to "migrate" users. This way we can make sure those parts are working properly too
|
||||
// @see http://stackoverflow.com/questions/14867697/mongoose-full-collection-scan
|
||||
// @see https://stackoverflow.com/questions/14867697/mongoose-full-collection-scan
|
||||
// Also, what do we think of a Mongoose Migration module? something like https://github.com/madhums/mongoose-migrate
|
||||
|
||||
// IMPORTANT NOTE: this migration was written when we were using version 3 of lodash.
|
||||
|
||||
@@ -19,7 +19,7 @@ const Timer = require('./utils/timer');
|
||||
const connectToDb = require('./utils/connect').connectToDb;
|
||||
const closeDb = require('./utils/connect').closeDb;
|
||||
|
||||
const message = '`This party\'s collection quest has been made easier! For details, refer to http://habitica.fandom.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
|
||||
const message = '`This party\'s collection quest has been made easier! For details, refer to https://habitica.fandom.com/wiki/User_blog:LadyAlys/Collection_Quests_are_Now_Easier`';
|
||||
|
||||
const timer = new Timer();
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20220524_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['Alligator-Base']
|
||||
&& pets['Alligator-CottonCandyBlue']
|
||||
&& pets['Alligator-CottonCandyPink']
|
||||
&& pets['Alligator-Desert']
|
||||
&& pets['Alligator-Golden']
|
||||
&& pets['Alligator-Red']
|
||||
&& pets['Alligator-Shade']
|
||||
&& pets['Alligator-Skeleton']
|
||||
&& pets['Alligator-White']
|
||||
&& pets['Alligator-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['Triceratops-Base']
|
||||
&& pets['Triceratops-CottonCandyBlue']
|
||||
&& pets['Triceratops-CottonCandyPink']
|
||||
&& pets['Triceratops-Desert']
|
||||
&& pets['Triceratops-Golden']
|
||||
&& pets['Triceratops-Red']
|
||||
&& pets['Triceratops-Shade']
|
||||
&& pets['Triceratops-Skeleton']
|
||||
&& pets['Triceratops-White']
|
||||
&& pets['Triceratops-Zombie']
|
||||
&& pets['TRex-Base']
|
||||
&& pets['TRex-CottonCandyBlue']
|
||||
&& pets['TRex-CottonCandyPink']
|
||||
&& pets['TRex-Desert']
|
||||
&& pets['TRex-Golden']
|
||||
&& pets['TRex-Red']
|
||||
&& pets['TRex-Shade']
|
||||
&& pets['TRex-Skeleton']
|
||||
&& pets['TRex-White']
|
||||
&& pets['TRex-Zombie']
|
||||
&& pets['Pterodactyl-Base']
|
||||
&& pets['Pterodactyl-CottonCandyBlue']
|
||||
&& pets['Pterodactyl-CottonCandyPink']
|
||||
&& pets['Pterodactyl-Desert']
|
||||
&& pets['Pterodactyl-Golden']
|
||||
&& pets['Pterodactyl-Red']
|
||||
&& pets['Pterodactyl-Shade']
|
||||
&& pets['Pterodactyl-Skeleton']
|
||||
&& pets['Pterodactyl-White']
|
||||
&& pets['Pterodactyl-Zombie']
|
||||
&& pets['Turtle-Base']
|
||||
&& pets['Turtle-CottonCandyBlue']
|
||||
&& pets['Turtle-CottonCandyPink']
|
||||
&& pets['Turtle-Desert']
|
||||
&& pets['Turtle-Golden']
|
||||
&& pets['Turtle-Red']
|
||||
&& pets['Turtle-Shade']
|
||||
&& pets['Turtle-Skeleton']
|
||||
&& pets['Turtle-White']
|
||||
&& pets['Turtle-Zombie']
|
||||
&& pets['Velociraptor-Base']
|
||||
&& pets['Velociraptor-CottonCandyBlue']
|
||||
&& pets['Velociraptor-CottonCandyPink']
|
||||
&& pets['Velociraptor-Desert']
|
||||
&& pets['Velociraptor-Golden']
|
||||
&& pets['Velociraptor-Red']
|
||||
&& pets['Velociraptor-Shade']
|
||||
&& pets['Velociraptor-Skeleton']
|
||||
&& pets['Velociraptor-White']
|
||||
&& pets['Velociraptor-Zombie']) {
|
||||
set['achievements.reptacularRumble'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
// migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2022-01-01') },
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
@@ -16,7 +16,7 @@ AWS.config.update({
|
||||
const BUCKET_NAME = config.S3.bucket;
|
||||
const s3 = new AWS.S3();
|
||||
|
||||
// Adapted from http://stackoverflow.com/a/22210077/2601552
|
||||
// Adapted from https://stackoverflow.com/a/22210077/2601552
|
||||
function uploadFile (buffer, fileName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
s3.putObject({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// For some reason people often to contact me to cancel their sub,
|
||||
// rather than do it online. Even when I point them to
|
||||
// the FAQ (http://goo.gl/1uoPGQ) they insist...
|
||||
// the FAQ (https://habitica.fandom.com/wiki/FAQ) they insist...
|
||||
|
||||
db.users.update(
|
||||
{ _id: '' },
|
||||
|
||||
Generated
+1246
-1270
File diff suppressed because it is too large
Load Diff
+16
-16
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.230.0",
|
||||
"version": "4.238.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.17.10",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/register": "^7.17.7",
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@babel/register": "^7.18.6",
|
||||
"@google-cloud/trace-agent": "^5.1.6",
|
||||
"@parse/node-apn": "^5.1.3",
|
||||
"@slack/webhook": "^6.1.0",
|
||||
"accepts": "^1.3.8",
|
||||
"amazon-payments": "^0.2.9",
|
||||
"amplitude": "^6.0.0",
|
||||
"apidoc": "^0.51.1",
|
||||
"apidoc": "^0.52.0",
|
||||
"apple-auth": "^1.0.7",
|
||||
"bcrypt": "^5.0.1",
|
||||
"body-parser": "^1.20.0",
|
||||
@@ -27,27 +27,27 @@
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.0.0",
|
||||
"express": "^4.17.3",
|
||||
"express": "^4.18.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^8.0.1",
|
||||
"glob": "^8.0.3",
|
||||
"got": "^11.8.3",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
"gulp-imagemin": "^7.1.0",
|
||||
"gulp-nodemon": "^2.5.0",
|
||||
"gulp.spritesmith": "^6.12.1",
|
||||
"gulp.spritesmith": "^6.13.0",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"helmet": "^4.6.0",
|
||||
"image-size": "^1.0.1",
|
||||
"image-size": "^1.0.2",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"js2xmlparser": "^4.0.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwks-rsa": "^2.1.0",
|
||||
"jwks-rsa": "^2.1.4",
|
||||
"lodash": "^4.17.21",
|
||||
"merge-stream": "^2.0.0",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.13.7",
|
||||
"morgan": "^1.10.0",
|
||||
@@ -64,17 +64,17 @@
|
||||
"rate-limiter-flexible": "^2.3.7",
|
||||
"redis": "^3.1.2",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.0",
|
||||
"stripe": "^8.219.0",
|
||||
"superagent": "^7.1.3",
|
||||
"stripe": "^8.222.0",
|
||||
"superagent": "^7.1.6",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.7.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.7.2",
|
||||
"winston": "^3.8.1",
|
||||
"winston-loggly-bulk": "^3.2.1",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
@@ -110,7 +110,7 @@
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"axios": "^0.27.2",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
|
||||
@@ -326,6 +326,36 @@ describe('Google Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription with multiple inactive subscriptions', async () => {
|
||||
const laterDate = moment.utc().add(7, 'days');
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ expirationDate: laterDate, autoRenewing: false },
|
||||
]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
nextBill: laterDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not cancel a user subscription with autorenew', async () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
@@ -346,5 +376,28 @@ describe('Google Payments', () => {
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.not.be.called;
|
||||
});
|
||||
|
||||
it('should not cancel a user subscription with multiple subscriptions with one autorenew', async () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate, autoRenewing: false },
|
||||
{ autoRenewing: true },
|
||||
{ expirationDate, autoRenewing: false }]);
|
||||
await googlePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
|
||||
data: receipt,
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,3 +42,109 @@ describe('xml marshaller marshalls user data', () => {
|
||||
</user>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('xml marshaller marshalls user data (with purchases)', () => {
|
||||
const minimumUser = {
|
||||
pinnedItems: [],
|
||||
unpinnedItems: [],
|
||||
inbox: {},
|
||||
};
|
||||
|
||||
function userDataWith (fields) {
|
||||
return { ...minimumUser, ...fields };
|
||||
}
|
||||
|
||||
it('maps the purchases field with data that begins with a number', () => {
|
||||
const userData = userDataWith({
|
||||
purchased: {
|
||||
ads: false,
|
||||
txnCount: 0,
|
||||
skin: {
|
||||
eb052b: true,
|
||||
'0ff591': true,
|
||||
'2b43f6': true,
|
||||
d7a9f7: true,
|
||||
'800ed0': true,
|
||||
rainbow: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const xml = xmlMarshaller.marshallUserData(userData);
|
||||
|
||||
expect(xml).to.equal(`<user>
|
||||
<inbox/>
|
||||
<purchased>
|
||||
<ads>false</ads>
|
||||
<txnCount>0</txnCount>
|
||||
<skin>eb052b</skin>
|
||||
<skin>0ff591</skin>
|
||||
<skin>2b43f6</skin>
|
||||
<skin>d7a9f7</skin>
|
||||
<skin>800ed0</skin>
|
||||
<skin>rainbow</skin>
|
||||
</purchased>
|
||||
</user>`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('xml marshaller marshalls user data (with purchases nested)', () => {
|
||||
const minimumUser = {
|
||||
pinnedItems: [],
|
||||
unpinnedItems: [],
|
||||
inbox: {},
|
||||
};
|
||||
|
||||
function userDataWith (fields) {
|
||||
return { ...minimumUser, ...fields };
|
||||
}
|
||||
|
||||
it('maps the purchases field with data that begins with a number and nested objects', () => {
|
||||
const userData = userDataWith({
|
||||
purchased: {
|
||||
ads: false,
|
||||
txnCount: 0,
|
||||
skin: {
|
||||
eb052b: true,
|
||||
'0ff591': true,
|
||||
'2b43f6': true,
|
||||
d7a9f7: true,
|
||||
'800ed0': true,
|
||||
rainbow: true,
|
||||
},
|
||||
plan: {
|
||||
consecutive: {
|
||||
count: 0,
|
||||
offset: 0,
|
||||
gemCapExtra: 0,
|
||||
trinkets: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const xml = xmlMarshaller.marshallUserData(userData);
|
||||
|
||||
expect(xml).to.equal(`<user>
|
||||
<inbox/>
|
||||
<purchased>
|
||||
<ads>false</ads>
|
||||
<txnCount>0</txnCount>
|
||||
<skin>eb052b</skin>
|
||||
<skin>0ff591</skin>
|
||||
<skin>2b43f6</skin>
|
||||
<skin>d7a9f7</skin>
|
||||
<skin>800ed0</skin>
|
||||
<skin>rainbow</skin>
|
||||
<plan>
|
||||
<item>
|
||||
<count>0</count>
|
||||
<offset>0</offset>
|
||||
<gemCapExtra>0</gemCapExtra>
|
||||
<trinkets>0</trinkets>
|
||||
</item>
|
||||
</plan>
|
||||
</purchased>
|
||||
</user>`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -634,7 +634,7 @@ describe('User Model', () => {
|
||||
user = await user.save();
|
||||
// verify that it's been awarded
|
||||
expect(user.achievements.beastMaster).to.equal(true);
|
||||
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
|
||||
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_STABLE')).to.exist;
|
||||
|
||||
// reset the user
|
||||
user.achievements.beastMasterCount = 0;
|
||||
@@ -683,9 +683,9 @@ describe('User Model', () => {
|
||||
|
||||
user = await user.save();
|
||||
// verify that it's been awarded
|
||||
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist;
|
||||
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_MOUNT_MASTER')).to.exist;
|
||||
expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_TRIAD_BINGO')).to.exist;
|
||||
expect(user.notifications.find(
|
||||
notification => notification.type === 'ACHIEVEMENT_STABLE',
|
||||
)).to.exist;
|
||||
});
|
||||
|
||||
context('manage unallocated stats points notifications', () => {
|
||||
|
||||
@@ -15,6 +15,10 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
},
|
||||
leaderDetails: {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
balance: 10,
|
||||
},
|
||||
});
|
||||
|
||||
groupWithChat = group;
|
||||
|
||||
@@ -117,7 +117,9 @@ describe('POST /chat/:chatId/flag', () => {
|
||||
});
|
||||
|
||||
it('Flags a chat when the author\'s account was deleted', async () => {
|
||||
const deletedUser = await generateUser();
|
||||
const deletedUser = await generateUser({
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
|
||||
await deletedUser.del('/user', {
|
||||
password: 'password',
|
||||
|
||||
@@ -18,11 +18,16 @@ describe('POST /chat/:chatId/like', () => {
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
leaderDetails: {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
balance: 10,
|
||||
},
|
||||
});
|
||||
|
||||
user = groupLeader;
|
||||
groupWithChat = group;
|
||||
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
|
||||
await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
});
|
||||
|
||||
it('Returns an error when chat message is not found', async () => {
|
||||
|
||||
@@ -38,10 +38,15 @@ describe('POST /chat', () => {
|
||||
members: 2,
|
||||
});
|
||||
user = groupLeader;
|
||||
await user.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL }); // prevent tests accidentally throwing messageGroupChatSpam
|
||||
await user.update({
|
||||
'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
}); // prevent tests accidentally throwing messageGroupChatSpam
|
||||
groupWithChat = group;
|
||||
member = members[0]; // eslint-disable-line prefer-destructuring
|
||||
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
|
||||
await member.update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
});
|
||||
|
||||
it('Returns an error when no message is provided', async () => {
|
||||
@@ -104,7 +109,10 @@ describe('POST /chat', () => {
|
||||
});
|
||||
|
||||
const privateGuildMemberWithChatsRevoked = members[0];
|
||||
await privateGuildMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
|
||||
await privateGuildMemberWithChatsRevoked.update({
|
||||
'flags.chatRevoked': true,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
@@ -122,7 +130,10 @@ describe('POST /chat', () => {
|
||||
});
|
||||
|
||||
const privatePartyMemberWithChatsRevoked = members[0];
|
||||
await privatePartyMemberWithChatsRevoked.update({ 'flags.chatRevoked': true });
|
||||
await privatePartyMemberWithChatsRevoked.update({
|
||||
'flags.chatRevoked': true,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const message = await privatePartyMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
@@ -183,7 +194,10 @@ describe('POST /chat', () => {
|
||||
});
|
||||
|
||||
const userWithChatShadowMuted = members[0];
|
||||
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
|
||||
await userWithChatShadowMuted.update({
|
||||
'flags.chatShadowMuted': true,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
@@ -202,7 +216,10 @@ describe('POST /chat', () => {
|
||||
});
|
||||
|
||||
const userWithChatShadowMuted = members[0];
|
||||
await userWithChatShadowMuted.update({ 'flags.chatShadowMuted': true });
|
||||
await userWithChatShadowMuted.update({
|
||||
'flags.chatShadowMuted': true,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
@@ -312,6 +329,7 @@ describe('POST /chat', () => {
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
|
||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
||||
|
||||
@@ -330,6 +348,7 @@ describe('POST /chat', () => {
|
||||
|
||||
// Update the bannedWordsAllowed property for the group
|
||||
group.update({ bannedWordsAllowed: true });
|
||||
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
|
||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
||||
|
||||
@@ -345,6 +364,7 @@ describe('POST /chat', () => {
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
|
||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
|
||||
|
||||
@@ -402,7 +422,7 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('does not allow slurs in private groups', async () => {
|
||||
it('allows slurs in private groups', async () => {
|
||||
const { group, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'Party',
|
||||
@@ -411,43 +431,11 @@ describe('POST /chat', () => {
|
||||
},
|
||||
members: 1,
|
||||
});
|
||||
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
|
||||
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage })).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('bannedSlurUsed'),
|
||||
});
|
||||
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testSlurMessage });
|
||||
|
||||
// Email sent to mods
|
||||
await sleep(0.5);
|
||||
expect(email.sendTxn).to.be.calledThrice;
|
||||
expect(email.sendTxn.args[2][1]).to.eql('slur-report-to-mods');
|
||||
|
||||
// Slack message to mods
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
|
||||
/* eslint-disable camelcase */
|
||||
expect(IncomingWebhook.prototype.send).to.be.calledWith({
|
||||
text: `${members[0].profile.name} (${members[0].id}) tried to post a slur`,
|
||||
attachments: [{
|
||||
fallback: 'Slur Message',
|
||||
color: 'danger',
|
||||
author_name: `@${members[0].auth.local.username} ${members[0].profile.name} (${members[0].auth.local.email}; ${members[0]._id})`,
|
||||
title: 'Slur in Party - (private party)',
|
||||
title_link: undefined,
|
||||
text: testSlurMessage,
|
||||
mrkdwn_in: [
|
||||
'text',
|
||||
],
|
||||
}],
|
||||
});
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
// Chat privileges are revoked
|
||||
await expect(members[0].post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('chatPrivilegesRevoked'),
|
||||
});
|
||||
expect(message.message.id).to.exist;
|
||||
});
|
||||
|
||||
it('errors when slur is typed in mixed case', async () => {
|
||||
@@ -463,6 +451,16 @@ describe('POST /chat', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when user account is too young', async () => {
|
||||
const brandNewUser = await generateUser();
|
||||
await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('chatTemporarilyUnavailable'),
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a chat', async () => {
|
||||
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
|
||||
@@ -525,6 +523,7 @@ describe('POST /chat', () => {
|
||||
'items.currentMount': mount,
|
||||
'items.currentPet': pet,
|
||||
'preferences.style': style,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
await userWithStyle.sync();
|
||||
|
||||
@@ -550,6 +549,7 @@ describe('POST /chat', () => {
|
||||
};
|
||||
const backer = await generateUser({
|
||||
backer: backerInfo,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
|
||||
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
@@ -620,6 +620,9 @@ describe('POST /chat', () => {
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
leaderDetails: {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
},
|
||||
});
|
||||
|
||||
const message = await groupLeader.post(`/groups/${group._id}/chat`, { message: testMessage });
|
||||
|
||||
@@ -15,6 +15,10 @@ describe('POST /groups/:id/chat/seen', () => {
|
||||
privacy: 'public',
|
||||
},
|
||||
members: 1,
|
||||
leaderDetails: {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
balance: 10,
|
||||
},
|
||||
});
|
||||
|
||||
guild = group;
|
||||
@@ -51,6 +55,9 @@ describe('POST /groups/:id/chat/seen', () => {
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
leaderDetails: {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
},
|
||||
});
|
||||
|
||||
party = group;
|
||||
|
||||
@@ -18,6 +18,10 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
},
|
||||
leaderDetails: {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
balance: 10,
|
||||
},
|
||||
});
|
||||
|
||||
groupWithChat = group;
|
||||
@@ -65,6 +69,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
members: 1,
|
||||
});
|
||||
|
||||
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
let privateMessage = await members[0].post(`/groups/${group._id}/chat`, { message: 'Some message' });
|
||||
privateMessage = privateMessage.message;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
describe('POST /group/:groupId/remove-manager', () => {
|
||||
let leader; let nonLeader; let
|
||||
groupToUpdate;
|
||||
const groupName = 'Test Public Guild';
|
||||
const groupName = 'Test Private Guild';
|
||||
const groupType = 'guild';
|
||||
let nonManager;
|
||||
|
||||
@@ -20,9 +20,10 @@ describe('POST /group/:groupId/remove-manager', () => {
|
||||
groupDetails: {
|
||||
name: groupName,
|
||||
type: groupType,
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
groupToUpdate = group;
|
||||
@@ -83,7 +84,7 @@ describe('POST /group/:groupId/remove-manager', () => {
|
||||
|
||||
await nonLeader.sync();
|
||||
|
||||
expect(nonLeader.notifications.length).to.equal(0);
|
||||
expect(nonLeader.notifications.length).to.equal(1); // user gets mystery items
|
||||
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
leader = groupLeader;
|
||||
member = members[0]; // eslint-disable-line prefer-destructuring
|
||||
memberCount = group.memberCount;
|
||||
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
});
|
||||
|
||||
it('prevents non members from leaving', async () => {
|
||||
@@ -152,6 +153,10 @@ describe('POST /groups/:groupId/leave', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
invites: 1,
|
||||
leaderDetails: {
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
balance: 10,
|
||||
},
|
||||
});
|
||||
|
||||
privateGuild = group;
|
||||
|
||||
@@ -153,6 +153,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
|
||||
},
|
||||
invites: 1,
|
||||
members: 2,
|
||||
leaderDetails: { 'auth.timestamps.created': new Date('2022-01-01') },
|
||||
});
|
||||
|
||||
party = group;
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('Prevent multiple notifications', () => {
|
||||
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
for (let memberIndex = 0; memberIndex < partyMembers.length; memberIndex += 1) {
|
||||
await partyMembers[memberIndex].update({ 'auth.timestamps.created': new Date('2022-01-01') }); // eslint-disable-line no-await-in-loop
|
||||
multipleChatMessages.push(
|
||||
partyMembers[memberIndex].post(`/groups/${party._id}/chat`, { message: `Message ${i}_${memberIndex}` }),
|
||||
);
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import superagent from 'superagent';
|
||||
import nconf from 'nconf';
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
|
||||
const API_TEST_SERVER_PORT = nconf.get('PORT');
|
||||
xdescribe('GET /qr-code/user/:memberId', () => {
|
||||
let user;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('validates req.params.memberId', async () => {
|
||||
await expect(user.get('/qr-code/user/invalidUUID')).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to profile page', async () => {
|
||||
const url = `http://localhost:${API_TEST_SERVER_PORT}/qr-code/user/${user._id}`;
|
||||
const response = await superagent.get(url).end((err, res) => {
|
||||
expect(err).to.be(undefined);
|
||||
return res;
|
||||
});
|
||||
expect(response.status).to.eql(200);
|
||||
expect(response.request.url).to.eql(`http://localhost:${API_TEST_SERVER_PORT}/static/front/#?memberId=${user._id}`);
|
||||
});
|
||||
});
|
||||
@@ -134,6 +134,7 @@ describe('GET /tasks/:id', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 1,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
group = groupData.group;
|
||||
|
||||
@@ -7,7 +7,11 @@ import {
|
||||
describe('POST /tasks/clearCompletedTodos', () => {
|
||||
it('deletes all completed todos except the ones from a challenge and group', async () => {
|
||||
const user = await generateUser({ balance: 1 });
|
||||
const guild = await generateGroup(user);
|
||||
const guild = await generateGroup(
|
||||
user,
|
||||
{},
|
||||
{ 'purchased.plan.customerId': 'group-unlimited' },
|
||||
);
|
||||
const challenge = await generateChallenge(user, guild);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
@@ -77,18 +78,18 @@ describe('Groups DELETE /tasks/:id', () => {
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications.length).to.equal(3); // mystery items
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(3);
|
||||
expect(member2.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
await member2.del(`/tasks/${task._id}`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
});
|
||||
|
||||
it('deletes task from assigned user', async () => {
|
||||
|
||||
@@ -18,6 +18,7 @@ describe('GET /approvals/group/:groupId', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('GET /tasks/group/:groupId', () => {
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
group = await generateGroup(user);
|
||||
group = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
|
||||
});
|
||||
|
||||
it('returns error when group is not found', async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
@@ -63,9 +64,9 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
@@ -89,9 +90,9 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
await member.sync();
|
||||
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
expect(member.notifications.length).to.equal(3);
|
||||
expect(member.notifications[2].type).to.equal('GROUP_TASK_APPROVED');
|
||||
expect(member.notifications[2].data.message).to.equal(t('yourTaskHasBeenApproved', { taskText: task.text }));
|
||||
|
||||
memberTasks = await member.get('/tasks/user');
|
||||
syncedTask = find(memberTasks, findAssignedTask);
|
||||
@@ -113,18 +114,18 @@ describe('POST /tasks/:id/approve/:userId', () => {
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
|
||||
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
|
||||
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(1);
|
||||
expect(member2.notifications.length).to.equal(0);
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
});
|
||||
|
||||
it('prevents double approval on a task', async () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
@@ -72,7 +73,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
// Check that the notification is correct
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 2);
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 3);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
@@ -121,7 +122,7 @@ describe('POST /tasks/:id/needs-work/:userId', () => {
|
||||
expect(syncedTask.group.approval.requested).to.equal(false);
|
||||
expect(syncedTask.group.approval.requestedDate).to.equal(undefined);
|
||||
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 2);
|
||||
expect(member.notifications.length).to.equal(initialNotifications + 3);
|
||||
const notification = member.notifications[member.notifications.length - 1];
|
||||
expect(notification.type).to.equal('GROUP_TASK_NEEDS_WORK');
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
@@ -53,15 +54,15 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
|
||||
await user.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}, 'cs')); // This test only works if we have the notification translated
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications[2].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(updatedTask.group.approval.requested).to.equal(true);
|
||||
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
|
||||
@@ -80,25 +81,25 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
await user.sync();
|
||||
await member2.sync();
|
||||
|
||||
expect(user.notifications.length).to.equal(2);
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(user.notifications.length).to.equal(3);
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(user.notifications[2].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications[2].data.groupId).to.equal(guild._id);
|
||||
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
expect(member2.notifications[1].type).to.equal('GROUP_TASK_APPROVAL');
|
||||
expect(member2.notifications[1].data.message).to.equal(t('userHasRequestedTaskApproval', {
|
||||
user: member.auth.local.username,
|
||||
taskName: updatedTask.text,
|
||||
taskId: updatedTask._id,
|
||||
direction,
|
||||
}));
|
||||
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
|
||||
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('errors when approval has already been requested', async () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ describe('POST /tasks/group/:groupid', () => {
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
@@ -103,14 +104,14 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
await member2.sync();
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(user.notifications.length).to.equal(2); // includes Guild Joined achievement
|
||||
expect(user.notifications[1].type).to.equal('GROUP_TASK_CLAIMED');
|
||||
expect(user.notifications[1].data.taskId).to.equal(groupTask[0]._id);
|
||||
expect(user.notifications[1].data.groupId).to.equal(guild._id);
|
||||
expect(member2.notifications.length).to.equal(1);
|
||||
expect(member2.notifications[0].type).to.equal('GROUP_TASK_CLAIMED');
|
||||
expect(member2.notifications[0].data.taskId).to.equal(groupTask[0]._id);
|
||||
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
|
||||
expect(user.notifications.length).to.equal(3); // includes Guild Joined achievement
|
||||
expect(user.notifications[2].type).to.equal('GROUP_TASK_CLAIMED');
|
||||
expect(user.notifications[2].data.taskId).to.equal(groupTask[0]._id);
|
||||
expect(user.notifications[2].data.groupId).to.equal(guild._id);
|
||||
expect(member2.notifications.length).to.equal(2);
|
||||
expect(member2.notifications[1].type).to.equal('GROUP_TASK_CLAIMED');
|
||||
expect(member2.notifications[1].data.taskId).to.equal(groupTask[0]._id);
|
||||
expect(member2.notifications[1].data.groupId).to.equal(guild._id);
|
||||
});
|
||||
|
||||
it('assigns a task to a user', async () => {
|
||||
@@ -130,9 +131,9 @@ describe('POST /tasks/:taskId/assign/:memberId', () => {
|
||||
|
||||
const groupTask = await user.get(`/tasks/group/${guild._id}`);
|
||||
|
||||
expect(member.notifications.length).to.equal(1);
|
||||
expect(member.notifications[0].type).to.equal('GROUP_TASK_ASSIGNED');
|
||||
expect(member.notifications[0].taskId).to.equal(groupTask._id);
|
||||
expect(member.notifications.length).to.equal(2);
|
||||
expect(member.notifications[1].type).to.equal('GROUP_TASK_ASSIGNED');
|
||||
expect(member.notifications[1].taskId).to.equal(groupTask._id);
|
||||
});
|
||||
|
||||
it('assigns a task to multiple users', async () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('POST group-tasks/:taskId/move/to/:position', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser({ balance: 1 });
|
||||
guild = await generateGroup(user, { type: 'guild' });
|
||||
guild = await generateGroup(user, { type: 'guild' }, { 'purchased.plan.customerId': 'group-unlimited' });
|
||||
});
|
||||
|
||||
it('can move task to new position', async () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
@@ -91,7 +92,7 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
|
||||
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
|
||||
|
||||
await member.sync();
|
||||
expect(member.notifications.length).to.equal(0);
|
||||
expect(member.notifications.length).to.equal(1); // mystery items
|
||||
});
|
||||
|
||||
it('unassigns a user and only that user from a task', async () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('PUT /tasks/:id', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ describe('DELETE group /tasks/:taskId/checklist/:itemId', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ describe('POST group /tasks/:taskId/checklist/', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ describe('PUT group /tasks/:taskId/checklist/:itemId', () => {
|
||||
type: 'guild',
|
||||
},
|
||||
members: 2,
|
||||
upgradeToGroupPlan: true,
|
||||
});
|
||||
|
||||
guild = group;
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
|
||||
it('returns an error if a group task was targeted', async () => {
|
||||
const { group, groupLeader } = await createAndPopulateGroup();
|
||||
const { group, groupLeader } = await createAndPopulateGroup({ upgradeToGroupPlan: true });
|
||||
|
||||
const groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
|
||||
text: 'todo group',
|
||||
@@ -266,7 +266,7 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
|
||||
it('searing brightness does not affect challenge or group tasks', async () => {
|
||||
const guild = await generateGroup(user);
|
||||
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
|
||||
const challenge = await generateChallenge(user, guild);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('POST /user/reset', () => {
|
||||
});
|
||||
|
||||
it('does not delete challenge or group tasks', async () => {
|
||||
const guild = await generateGroup(user);
|
||||
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
|
||||
const challenge = await generateChallenge(user, guild);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
|
||||
@@ -39,25 +39,38 @@ describe('POST /user/buy-quest/:key', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns an error if quest prerequisites are not met', async () => {
|
||||
const key = 'dilatoryDistress2';
|
||||
it('returns an error if not all quest prerequisites are met', async () => {
|
||||
const prerequisites = ['dilatoryDistress1', 'dilatoryDistress2'];
|
||||
const key = 'dilatoryDistress3';
|
||||
|
||||
const achievementName1 = `achievements.quests.${prerequisites[0]}`;
|
||||
|
||||
await user.update({
|
||||
[achievementName1]: true,
|
||||
'stats.gp': 9999,
|
||||
});
|
||||
|
||||
await expect(user.post(`/user/buy-quest/${key}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('mustComplete', { quest: 'dilatoryDistress1' }),
|
||||
message: t('mustComplete', { quest: prerequisites[1] }),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows purchase of a quest if prerequisites are met', async () => {
|
||||
const prerequisite = 'dilatoryDistress1';
|
||||
const key = 'dilatoryDistress2';
|
||||
const prerequisites = ['dilatoryDistress1', 'dilatoryDistress2'];
|
||||
const key = 'dilatoryDistress3';
|
||||
const item = content.quests[key];
|
||||
|
||||
const achievementName = `achievements.quests.${prerequisite}`;
|
||||
const achievementName1 = `achievements.quests.${prerequisites[0]}`;
|
||||
const achievementName2 = `achievements.quests.${prerequisites[1]}`;
|
||||
|
||||
await user.update({ [achievementName]: true, 'stats.gp': 9999 });
|
||||
await user.update({
|
||||
[achievementName1]: true,
|
||||
[achievementName2]: true,
|
||||
'stats.gp': 9999,
|
||||
});
|
||||
const res = await user.post(`/user/buy-quest/${key}`);
|
||||
await user.sync();
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
|
||||
it('returns an error if a group task was targeted', async () => {
|
||||
const { group, groupLeader } = await createAndPopulateGroup();
|
||||
const { group, groupLeader } = await createAndPopulateGroup({ upgradeToGroupPlan: true });
|
||||
|
||||
const groupTask = await groupLeader.post(`/tasks/group/${group._id}`, {
|
||||
text: 'todo group',
|
||||
@@ -234,7 +234,7 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
});
|
||||
|
||||
it('searing brightness does not affect challenge or group tasks', async () => {
|
||||
const guild = await generateGroup(user);
|
||||
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
|
||||
const challenge = await generateChallenge(user, guild);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
|
||||
@@ -88,7 +88,7 @@ describe('POST /user/reset', () => {
|
||||
});
|
||||
|
||||
it('does not delete challenge or group tasks', async () => {
|
||||
const guild = await generateGroup(user);
|
||||
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
|
||||
const challenge = await generateChallenge(user, guild);
|
||||
await user.post(`/challenges/${challenge._id}/join`);
|
||||
await user.post(`/tasks/challenge/${challenge._id}`, {
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
});
|
||||
|
||||
context('successful purchase', () => {
|
||||
context('single purchase', () => {
|
||||
const userGemAmount = 10;
|
||||
|
||||
before(() => {
|
||||
@@ -44,7 +44,7 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
user.pinnedItems.push({ type: 'quests', key: 'gryphon' });
|
||||
});
|
||||
|
||||
it('purchases quests', async () => {
|
||||
it('successfully purchases quest', async () => {
|
||||
const key = 'gryphon';
|
||||
|
||||
await buyQuest(user, { params: { key } });
|
||||
@@ -58,6 +58,28 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
|
||||
await buyQuest(user, { params: { key } });
|
||||
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
it('errors if the user has not completed prerequisite quests', async () => {
|
||||
const key = 'atom3';
|
||||
user.achievements.quests.atom1 = 1;
|
||||
|
||||
try {
|
||||
await buyQuest(user, { params: { key } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('mustComplete', { quest: 'atom2' }));
|
||||
expect(user.items.quests[key]).to.eql(undefined);
|
||||
}
|
||||
});
|
||||
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
|
||||
const key = 'atom3';
|
||||
user.achievements.quests.atom1 = 1;
|
||||
user.achievements.quests.atom2 = 1;
|
||||
|
||||
await buyQuest(user, { params: { key } });
|
||||
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
});
|
||||
|
||||
@@ -162,7 +162,9 @@ describe('shared.ops.buyQuest', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('does not buy a quest without completing previous quests', async () => {
|
||||
it('returns error if user has not completed all prerequisite quests', async () => {
|
||||
user.stats.gp = 9999;
|
||||
user.achievements.quests.dilatoryDistress1 = 1;
|
||||
try {
|
||||
await buyQuest(user, {
|
||||
params: {
|
||||
@@ -175,4 +177,22 @@ describe('shared.ops.buyQuest', () => {
|
||||
expect(user.items.quests).to.eql({});
|
||||
}
|
||||
});
|
||||
|
||||
it('successfully purchases quest if user has completed all prerequisite quests', async () => {
|
||||
user.stats.gp = 500;
|
||||
user.achievements.quests.dilatoryDistress1 = 1;
|
||||
user.achievements.quests.dilatoryDistress2 = 1;
|
||||
|
||||
await buyQuest(user, {
|
||||
params: {
|
||||
key: 'dilatoryDistress3',
|
||||
},
|
||||
}, analytics);
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress3: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ app.post('/webhooks/:id', (req, res) => {
|
||||
});
|
||||
|
||||
// Helps close down server from within mocha test
|
||||
// See http://stackoverflow.com/a/37054753/2601552
|
||||
// See https://stackoverflow.com/a/37054753/2601552
|
||||
const sockets = {};
|
||||
server.on('connection', socket => {
|
||||
const id = uuid();
|
||||
|
||||
@@ -40,7 +40,6 @@ function _requestMaker (user, method, additionalSets = {}) {
|
||||
|| route.indexOf('/paypal') === 0
|
||||
|| route.indexOf('/amazon') === 0
|
||||
|| route.indexOf('/stripe') === 0
|
||||
|| route.indexOf('/qr-code') === 0
|
||||
|| route.indexOf('/analytics') === 0
|
||||
) {
|
||||
url += `${route}`;
|
||||
|
||||
@@ -5,6 +5,8 @@ import { v4 as generateUUID } from 'uuid';
|
||||
import { ApiUser, ApiGroup, ApiChallenge } from '../api-classes';
|
||||
import { requester } from '../requester';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import payments from '../../../../website/server/libs/payments/payments';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
|
||||
// Creates a new user and returns it
|
||||
// If you need the user to have specific requirements,
|
||||
@@ -77,6 +79,26 @@ export async function generateGroup (leader, details = {}, update = {}) {
|
||||
return apiGroup;
|
||||
}
|
||||
|
||||
async function _upgradeToGroupPlan (groupLeader, group) {
|
||||
const groupLeaderModel = await User.findById(groupLeader._id).exec();
|
||||
|
||||
// Create subscription
|
||||
const paymentData = {
|
||||
user: groupLeaderModel,
|
||||
groupId: group._id,
|
||||
sub: {
|
||||
key: 'basic_3mo',
|
||||
},
|
||||
customerId: 'customer-id',
|
||||
paymentMethod: 'Payment Method',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
};
|
||||
await payments.createSubscription(paymentData);
|
||||
}
|
||||
|
||||
// This is generate group + the ability to create
|
||||
// real users to populate it. The settings object
|
||||
// takes in:
|
||||
@@ -95,6 +117,7 @@ export async function generateGroup (leader, details = {}, update = {}) {
|
||||
export async function createAndPopulateGroup (settings = {}) {
|
||||
const numberOfMembers = settings.members || 0;
|
||||
const numberOfInvites = settings.invites || 0;
|
||||
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
|
||||
const { groupDetails } = settings;
|
||||
const leaderDetails = settings.leaderDetails || { balance: 10 };
|
||||
|
||||
@@ -124,6 +147,10 @@ export async function createAndPopulateGroup (settings = {}) {
|
||||
|
||||
await Promise.all(invitees.map(invitee => invitee.sync()));
|
||||
|
||||
if (upgradeToGroupPlan) {
|
||||
await _upgradeToGroupPlan(groupLeader, group);
|
||||
}
|
||||
|
||||
return {
|
||||
groupLeader,
|
||||
group,
|
||||
|
||||
@@ -11,6 +11,7 @@ if (process.env.LOAD_SERVER === '0') { // when the server is in a different proc
|
||||
setupNconf('./config.json.example');
|
||||
nconf.set('NODE_DB_URI', nconf.get('TEST_DB_URI'));
|
||||
nconf.set('NODE_ENV', 'test');
|
||||
nconf.set('ACCOUNT_MIN_CHAT_AGE', '2');
|
||||
nconf.set('IS_TEST', true);
|
||||
// We require src/server and not src/index because
|
||||
// 1. nconf is already setup
|
||||
|
||||
Generated
+398
-1039
File diff suppressed because it is too large
Load Diff
+14
-13
@@ -13,26 +13,27 @@
|
||||
"storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addon-actions": "6.4.19",
|
||||
"@storybook/addon-actions": "6.5.8",
|
||||
"@storybook/addon-knobs": "6.2.9",
|
||||
"@storybook/addon-links": "6.4.18",
|
||||
"@storybook/addon-links": "6.5.8",
|
||||
"@storybook/addon-notes": "5.3.21",
|
||||
"@storybook/addons": "6.4.19",
|
||||
"@storybook/addons": "6.5.9",
|
||||
"@storybook/vue": "6.3.13",
|
||||
"@vue/cli-plugin-babel": "^4.5.15",
|
||||
"@vue/cli-plugin-eslint": "^4.5.15",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-plugin-router": "^4.5.15",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.15",
|
||||
"@vue/cli-service": "^4.5.15",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.17.0",
|
||||
"amplitude-js": "^8.18.5",
|
||||
"axios": "^0.25.0",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"chai": "^4.3.6",
|
||||
"core-js": "^3.21.0",
|
||||
"core-js": "^3.23.5",
|
||||
"dompurify": "^2.3.8",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
@@ -40,14 +41,14 @@
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.19.5",
|
||||
"inspectpack": "^4.7.1",
|
||||
"intro.js": "^5.0.0",
|
||||
"intro.js": "^5.1.0",
|
||||
"jquery": "^3.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"nconf": "^0.11.3",
|
||||
"moment": "^2.29.4",
|
||||
"nconf": "^0.12.0",
|
||||
"sass": "^1.34.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.17.0",
|
||||
"smartbanner.js": "^1.19.0",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"svg-url-loader": "^7.1.1",
|
||||
"svgo": "^1.3.2",
|
||||
@@ -57,13 +58,13 @@
|
||||
"vue": "^2.6.14",
|
||||
"vue-cli-plugin-storybook": "2.1.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-router": "^3.5.4",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
|
||||
"webpack": "^4.46.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.16.7"
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +414,15 @@ export default {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
Analytics.setUser();
|
||||
Analytics.updateUser();
|
||||
return axios.get('/api/v4/i18n/browser-script', { language: this.user.preferences.language });
|
||||
return axios.get('/api/v4/i18n/browser-script',
|
||||
{
|
||||
language: this.user.preferences.language,
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
},
|
||||
});
|
||||
}).then(() => {
|
||||
const i18nData = window && window['habitica-i18n'];
|
||||
this.$loadLocale(i18nData);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,7 +82,7 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
}
|
||||
}
|
||||
|
||||
/** Colored Input-Groups, ignoring checklist */
|
||||
// Colored Input-Groups, ignoring checklist
|
||||
.input-group:not(.checklist-group) {
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
@@ -100,7 +100,7 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generic Input Group Styles */
|
||||
// Generic Input Group Styles
|
||||
.input-group {
|
||||
height: 2rem;
|
||||
|
||||
@@ -179,10 +179,11 @@ input, textarea, input.form-control, textarea.form-control {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
// Checkboxes and radios
|
||||
// used in checkboxes and radios
|
||||
$bg-focused-active-control: #4f2993;
|
||||
$bg-disabled-control: #34303a;
|
||||
|
||||
// custom control
|
||||
.custom-control {
|
||||
margin-bottom: .5rem;
|
||||
|
||||
@@ -205,6 +206,7 @@ $bg-disabled-control: #34303a;
|
||||
}
|
||||
}
|
||||
|
||||
// checkboxes
|
||||
.custom-checkbox {
|
||||
.custom-control-label::before {
|
||||
border-radius: 2px;
|
||||
@@ -280,11 +282,26 @@ $bg-disabled-control: #34303a;
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
// radio buttons
|
||||
$bg-color: $purple-400;
|
||||
|
||||
// svg for the purple dot
|
||||
@mixin custom-radio-checked-icon ($bg-color) {
|
||||
background-image: str-replace(url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='#{$bg-color}'/%3E%3C/svg%3E"), "#", "%23");
|
||||
}
|
||||
|
||||
.custom-radio .custom-control-input {
|
||||
opacity: 0;
|
||||
margin: 15px 25px 34px 25px;
|
||||
|
||||
// outside circle
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $gray-700;
|
||||
background-size: 12px 12px;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
// checked indicator
|
||||
&:checked~.custom-control-label::after {
|
||||
@include custom-radio-checked-icon($purple-400);
|
||||
width: 18px;
|
||||
@@ -292,51 +309,84 @@ $bg-disabled-control: #34303a;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
|
||||
&:checked~.custom-control-label::before {
|
||||
background-color: $gray-700;
|
||||
background-size: 12px 12px;
|
||||
border-color: $purple-400;
|
||||
}
|
||||
|
||||
&:active~.custom-control-label::before {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:focus:not(:checked):not(:disabled)~.custom-control-label::before, &:active:not(:checked):not(:disabled)~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
|
||||
// focus / not checked / not disabled
|
||||
&: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, &:active:checked:not(:disabled)~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 6px rgba($bg-focused-active-control, 0.1);
|
||||
border-color: $purple-400;
|
||||
background-color: rgba($bg-focused-active-control, 0.1);
|
||||
// focus / checked / not disabled
|
||||
&:focus:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:active:checked:not(:disabled)~.custom-control-label::before {
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
|
||||
}
|
||||
|
||||
&:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
background-color: transparent;
|
||||
// hover / not checked / not disabled
|
||||
&:hover:not(:checked):not(:disabled)~.custom-control-label::before,
|
||||
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: 50%/50% 50% no-repeat;
|
||||
@include custom-radio-checked-icon($purple-400);
|
||||
background-size: 12px 12px;
|
||||
border: solid 2px $purple-400;
|
||||
}
|
||||
|
||||
&:disabled:checked~.custom-control-label::after {
|
||||
// hover / checked / not disabled
|
||||
&:hover:checked:not(:disabled)~.custom-control-label::before,
|
||||
&:active::checked:not(:disabled)~.custom-control-label::before {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: 50%/50% 50% no-repeat;
|
||||
@include custom-radio-checked-icon($gray-400);
|
||||
background-size: 12px 12px;
|
||||
border: solid 2px $purple-300;
|
||||
}
|
||||
|
||||
// disabled / checked / before
|
||||
&:disabled:checked~.custom-control-label::before {
|
||||
background: 50%/50% 50% no-repeat;
|
||||
@include custom-radio-checked-icon($gray-300);
|
||||
border: 2px solid $gray-200;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
// disabled / checked / after
|
||||
&:disabled:checked~.custom-control-label::after {
|
||||
background: 50%/50% 50% no-repeat;
|
||||
@include custom-radio-checked-icon($gray-300);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: 12px 12px;
|
||||
}
|
||||
|
||||
// disabled / not checked / before
|
||||
&:disabled:not(:checked)~.custom-control-label::before {
|
||||
border-color: $gray-300;
|
||||
background-color: transparent;
|
||||
background-color: $gray-600;
|
||||
border: 2px solid $gray-200;
|
||||
}
|
||||
|
||||
&:focus:disabled~.custom-control-label::before, &:active:disabled~.custom-control-label::before {
|
||||
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
|
||||
border-color: $gray-300;
|
||||
// focus and disabled / not checked / before
|
||||
&:focus:disabled~.custom-control-label::before,
|
||||
&:active:disabled~.custom-control-label::before {
|
||||
background-color: rgba($bg-disabled-control, 0.1);
|
||||
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
|
||||
border: 2px solid $gray-200;
|
||||
}
|
||||
|
||||
&:focus:disabled:checked~.custom-control-label::before, &:active:disabled:checked~.custom-control-label::before {
|
||||
border-color: $gray-400;
|
||||
// focus and disabled / checked / before
|
||||
&:focus:disabled:checked~.custom-control-label::before,
|
||||
&:active:disabled:checked~.custom-control-label::before {
|
||||
background: 50%/50% 50% no-repeat;
|
||||
@include custom-radio-checked-icon($gray-300);
|
||||
border: 2px solid $gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@ body {
|
||||
// Restore the default styling for a elements without an href attribute
|
||||
// that was changed in bootstrap 4.5.1
|
||||
a:not([href]), a:not([href]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
&:not([role=button]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a, a:not([href]):not([tabindex]) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<svg width="176" height="67" viewBox="0 0 176 67" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#77F4C7" d="M35.667 11.667 40 9.5l-4.333-2.167L33.5 3l-2.167 4.333L27 9.5l4.333 2.167L33.5 16z"/>
|
||||
<path fill="#BDA8FF" d="M24.667 38.667 30 36l-5.333-2.667L22 28l-2.667 5.333L14 36l5.333 2.667L22 44z"/>
|
||||
<path fill="#8EEDF6" d="M35.667 63.667 39 62l-3.333-1.667L34 57l-1.667 3.333L29 62l3.333 1.667L34 67z"/>
|
||||
<path fill="#FFBE5D" d="M6.667 49.667 10 48l-3.333-1.667L5 43l-1.667 3.333L0 48l3.333 1.667L5 53z"/>
|
||||
<path fill="#FFB6B8" d="M5.667 20.667 8 19.5l-2.333-1.167L4.5 16l-1.167 2.333L1 19.5l2.333 1.167L4.5 23z"/>
|
||||
<g>
|
||||
<path fill="#77F4C7" d="M140.333 11.667 136 9.5l4.333-2.167L142.5 3l2.167 4.333L149 9.5l-4.333 2.167L142.5 16z"/>
|
||||
<path fill="#BDA8FF" d="M151.333 38.667 146 36l5.333-2.667L154 28l2.667 5.333L162 36l-5.333 2.667L154 44z"/>
|
||||
<path fill="#8EEDF6" d="M140.333 63.667 137 62l3.333-1.667L142 57l1.667 3.333L147 62l-3.333 1.667L142 67z"/>
|
||||
<path fill="#FFBE5D" d="M169.333 49.667 166 48l3.333-1.667L171 43l1.667 3.333L176 48l-3.333 1.667L171 53z"/>
|
||||
<path fill="#FFB6B8" d="M170.333 20.667 168 19.5l2.333-1.167L171.5 16l1.167 2.333L175 19.5l-2.333 1.167L171.5 23z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M81.117 13.904c-2.139-4.838-6.274-9.113-11.25-9.324-4.976-.211-7.828 3.779-6.367 7.309 1.461 3.53 4.94 4.177 16.227 7.202 3.204.858 3.528-.35 1.39-5.187z" stroke="#22AEB7" stroke-width="4"/>
|
||||
<path d="M93.833 13.904c2.138-4.838 6.273-9.113 11.25-9.324 4.975-.211 7.828 3.779 6.367 7.309-1.462 3.53-4.94 4.177-16.227 7.202-3.205.858-3.528-.35-1.39-5.187z" stroke="#38C9C6" stroke-width="4"/>
|
||||
<path d="M87.128 11c-9.738 0-3.907 11.145 0 11.145 3.908 0 9.74-11.145 0-11.145z" fill="#46DDDA"/>
|
||||
<path fill="#6133B4" d="M62 33h52v34H62zM56 21h64v12H56z"/>
|
||||
<path fill-opacity=".5" fill="#FFF" style="mix-blend-mode:soft-light" d="M32 30h26v34H32z" transform="translate(56 3)"/>
|
||||
<path fill="#8EEDF6" d="M88 33h6v34h-6z"/>
|
||||
<path fill="#3BCAD7" d="M82 33h6v34h-6zM76 21h12v12H76z"/>
|
||||
<path fill="#8EEDF6" d="M88 21h12v12H88z"/>
|
||||
<path fill-opacity=".2" fill="#000" style="mix-blend-mode:multiply" d="M6 30h26v6H6zM20 18h12v6H20zM0 24h20v6H0zM44 24h20v6H44zM32 18h12v6H32zM6 58h26v6H6zM32 30h26v6H32zM32 58h26v6H32z" transform="translate(56 3)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,5 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="2">
|
||||
<path d="M1 11L11 1M11 11L1 1"/>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon/Close</title>
|
||||
<g id="Modals" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Shop-Modals" transform="translate(-183.000000, -655.000000)" fill="#878190" fill-rule="nonzero">
|
||||
<g id="Icon/Close" transform="translate(183.000000, 655.000000)">
|
||||
<polygon id="Mask" points="12.1973467 2 14 3.80265326 9.80187117 8 14 12.1973467 12.1973467 14 8 9.80187117 3.80265326 14 2 12.1973467 6.19812883 8 2 3.80265326 3.80265326 2 8 6.19812883"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 747 B |
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="just-add-water"
|
||||
:title="title"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div class="col-12">
|
||||
<achievement-avatar class="avatar" />
|
||||
</div>
|
||||
<div class="col-6 offset-3 text-center">
|
||||
<p>{{ $t('achievementJustAddWaterModalText') }}</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('huzzah') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<achievement-footer />
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementJustAddWater')}`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'just-add-water');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="lost-masterclasser"
|
||||
:title="title"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div class="col-12">
|
||||
<achievement-avatar class="avatar" />
|
||||
</div>
|
||||
<div class="col-6 offset-3 text-center">
|
||||
<p>{{ $t('achievementLostMasterclasserModalText') }}</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('huzzah') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<achievement-footer />
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementLostMasterclasser')}`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'lost-masterclasser');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="mind-over-matter"
|
||||
:title="title"
|
||||
size="md"
|
||||
:hide-footer="true"
|
||||
>
|
||||
<div class="modal-body">
|
||||
<div class="col-12">
|
||||
<achievement-avatar class="avatar" />
|
||||
</div>
|
||||
<div class="col-6 offset-3 text-center">
|
||||
<p>{{ $t('achievementMindOverMatterModalText') }}</p>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('huzzah') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<achievement-footer />
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.avatar {
|
||||
width: 140px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1.5em;
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import achievementFooter from './achievementFooter';
|
||||
import achievementAvatar from './achievementAvatar';
|
||||
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
achievementFooter,
|
||||
achievementAvatar,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
title: `${this.$t('modalAchievement')} ${this.$t('achievementMindOverMatter')}`,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'mind-over-matter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -34,7 +34,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="http://blog.habitrpg.com/"
|
||||
href="https://blog.habitrpg.com/"
|
||||
target="_blank"
|
||||
>{{ $t('tumblr') }}</a>
|
||||
</li>
|
||||
|
||||
@@ -614,6 +614,7 @@ import axios from 'axios';
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { buildAppleAuthUrl } from '../../libs/auth';
|
||||
|
||||
import { MINIMUM_PASSWORD_LENGTH } from '@/../../common/script/constants';
|
||||
@@ -754,6 +755,12 @@ export default {
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
sanitizeRedirect (redirect) {
|
||||
if (!redirect) return '/';
|
||||
let sanitizedString = DOMPurify.sanitize(redirect).replace(/\\|\/\/|\./g, '');
|
||||
sanitizedString = `/${sanitizedString}`;
|
||||
return sanitizedString;
|
||||
},
|
||||
async register () {
|
||||
// @TODO do not use alert
|
||||
if (!this.email) {
|
||||
@@ -785,19 +792,14 @@ export default {
|
||||
passwordConfirm: this.passwordConfirm,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
const redirectTo = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data
|
||||
// is reloaded and correct for the logged in user
|
||||
// Same situation in login and socialAuth functions
|
||||
window.location.href = redirectTo;
|
||||
},
|
||||
async login () {
|
||||
@@ -807,19 +809,8 @@ export default {
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
const redirectTo = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data
|
||||
// is reloaded and correct for the logged in user
|
||||
window.location.href = redirectTo;
|
||||
},
|
||||
// @TODO: Abstract hello in to action or lib
|
||||
@@ -842,19 +833,8 @@ export default {
|
||||
auth,
|
||||
});
|
||||
|
||||
let redirectTo;
|
||||
const redirectTo = this.sanitizeRedirect(this.$route.query.redirectTo);
|
||||
|
||||
if (this.$route.query.redirectTo) {
|
||||
redirectTo = this.$route.query.redirectTo;
|
||||
} else {
|
||||
redirectTo = '/';
|
||||
}
|
||||
|
||||
// @TODO do not reload entire page
|
||||
// problem is that app.vue created hook should be called again
|
||||
// after user is logged in / just signed up
|
||||
// ALSO it's the only way to make sure language data
|
||||
// is reloaded and correct for the logged in user
|
||||
window.location.href = redirectTo;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -122,6 +122,11 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.box:hover {
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
|
||||
@@ -225,6 +225,11 @@ export default {
|
||||
this.group = await this.$store.dispatch('guilds:getGroup', {
|
||||
groupId: this.searchId,
|
||||
});
|
||||
if (!this.group?.purchased?.active) {
|
||||
if (this.group.type === 'guild') this.$router.push(`/groups/guild/${this.group._id}`);
|
||||
if (this.group.type === 'party') this.$router.push('/party');
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
subSection: this.group.name,
|
||||
section: this.$route.path.startsWith('/group-plans') ? this.$t('groupPlans') : this.$t('group'),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<creator-intro />
|
||||
<profileModal />
|
||||
<report-flag-modal />
|
||||
<send-gems-modal />
|
||||
<send-gift-modal />
|
||||
<select-user-modal />
|
||||
<b-navbar
|
||||
id="habitica-menu"
|
||||
@@ -747,7 +747,7 @@ import creatorIntro from '../creatorIntro';
|
||||
import notificationMenu from './notificationsDropdown';
|
||||
import profileModal from '../userMenu/profileModal';
|
||||
import reportFlagModal from '../chat/reportFlagModal';
|
||||
import sendGemsModal from '@/components/payments/sendGemsModal';
|
||||
import sendGiftModal from '@/components/payments/sendGiftModal';
|
||||
import selectUserModal from '@/components/payments/selectUserModal';
|
||||
import sync from '@/mixins/sync';
|
||||
import userDropdown from './userDropdown';
|
||||
@@ -759,7 +759,7 @@ export default {
|
||||
notificationMenu,
|
||||
profileModal,
|
||||
reportFlagModal,
|
||||
sendGemsModal,
|
||||
sendGiftModal,
|
||||
selectUserModal,
|
||||
userDropdown,
|
||||
},
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<base-notification
|
||||
:can-remove="canRemove"
|
||||
:notification="notification"
|
||||
:read-after-click="true"
|
||||
@click="action"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
v-html="achievementString"
|
||||
></div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
props: ['notification', 'canRemove'],
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementJustAddWater')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'just-add-water');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<base-notification
|
||||
:can-remove="canRemove"
|
||||
:notification="notification"
|
||||
:read-after-click="true"
|
||||
@click="action"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
v-html="achievementString"
|
||||
></div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
props: ['notification', 'canRemove'],
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementLostMasterclasser')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'lost-masterclasser');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<base-notification
|
||||
:can-remove="canRemove"
|
||||
:notification="notification"
|
||||
:read-after-click="true"
|
||||
@click="action"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
v-html="achievementString"
|
||||
></div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
props: ['notification', 'canRemove'],
|
||||
computed: {
|
||||
achievementString () {
|
||||
return `<strong>${this.$t('achievement')}</strong>: ${this.$t('achievementMindOverMatter')}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('bv::show::modal', 'mind-over-matter');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -140,9 +140,6 @@ import NEW_INBOX_MESSAGE from './notifications/newPrivateMessage';
|
||||
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
|
||||
import WORLD_BOSS from './notifications/worldBoss';
|
||||
import VERIFY_USERNAME from './notifications/verifyUsername';
|
||||
import ACHIEVEMENT_JUST_ADD_WATER from './notifications/justAddWater';
|
||||
import ACHIEVEMENT_LOST_MASTERCLASSER from './notifications/lostMasterclasser';
|
||||
import ACHIEVEMENT_MIND_OVER_MATTER from './notifications/mindOverMatter';
|
||||
import ONBOARDING_COMPLETE from './notifications/onboardingComplete';
|
||||
import GIFT_ONE_GET_ONE from './notifications/g1g1';
|
||||
import OnboardingGuide from './onboardingGuide';
|
||||
@@ -167,9 +164,6 @@ export default {
|
||||
CARD_RECEIVED,
|
||||
NEW_INBOX_MESSAGE,
|
||||
NEW_CHAT_MESSAGE,
|
||||
ACHIEVEMENT_JUST_ADD_WATER,
|
||||
ACHIEVEMENT_LOST_MASTERCLASSER,
|
||||
ACHIEVEMENT_MIND_OVER_MATTER,
|
||||
WorldBoss: WORLD_BOSS,
|
||||
VERIFY_USERNAME,
|
||||
OnboardingGuide,
|
||||
@@ -194,13 +188,24 @@ export default {
|
||||
// listed in the order they should appear in the notifications panel.
|
||||
// NOTE: Those not listed here won't be shown in the notification panel!
|
||||
handledNotifications: [
|
||||
'NEW_STUFF', 'GIFT_ONE_GET_ONE', 'GROUP_TASK_NEEDS_WORK',
|
||||
'GUILD_INVITATION', 'PARTY_INVITATION', 'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION', 'GROUP_TASK_ASSIGNED', 'GROUP_TASK_APPROVAL', 'GROUP_TASK_APPROVED',
|
||||
'GROUP_TASK_CLAIMED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
|
||||
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
|
||||
'ACHIEVEMENT_JUST_ADD_WATER', 'ACHIEVEMENT_LOST_MASTERCLASSER', 'ACHIEVEMENT_MIND_OVER_MATTER',
|
||||
'VERIFY_USERNAME', 'ONBOARDING_COMPLETE',
|
||||
'NEW_STUFF',
|
||||
'GIFT_ONE_GET_ONE',
|
||||
'GROUP_TASK_NEEDS_WORK',
|
||||
'GUILD_INVITATION',
|
||||
'PARTY_INVITATION',
|
||||
'CHALLENGE_INVITATION',
|
||||
'QUEST_INVITATION',
|
||||
'GROUP_TASK_ASSIGNED',
|
||||
'GROUP_TASK_APPROVAL',
|
||||
'GROUP_TASK_APPROVED',
|
||||
'GROUP_TASK_CLAIMED',
|
||||
'NEW_MYSTERY_ITEMS',
|
||||
'CARD_RECEIVED',
|
||||
'NEW_INBOX_MESSAGE',
|
||||
'NEW_CHAT_MESSAGE',
|
||||
'UNALLOCATED_STATS_POINTS',
|
||||
'VERIFY_USERNAME',
|
||||
'ONBOARDING_COMPLETE',
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:class="{ condensed, expanded, 'd-flex': isHeader, row: !isHeader, }"
|
||||
@click="showMemberModal(member)"
|
||||
>
|
||||
<div :class="{ 'col-4': !isHeader }">
|
||||
<div class="avatar-container" :class="{ 'col-4': !isHeader }">
|
||||
<avatar
|
||||
:member="member"
|
||||
:hide-class-badge="classBadgePosition !== 'under-avatar'"
|
||||
@@ -92,6 +92,10 @@
|
||||
.member-details {
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s ease-out;
|
||||
|
||||
.avatar-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.member-stats {
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
v-if="notificationData && notificationData.achievement"
|
||||
:data="notificationData"
|
||||
/>
|
||||
<just-add-water />
|
||||
<lost-masterclasser />
|
||||
<mind-over-matter />
|
||||
<onboarding-complete />
|
||||
<first-drops />
|
||||
</div>
|
||||
@@ -141,25 +138,34 @@ import streak from './achievements/streak';
|
||||
import ultimateGear from './achievements/ultimateGear';
|
||||
import wonChallenge from './achievements/wonChallenge';
|
||||
import genericAchievement from './achievements/genericAchievement';
|
||||
import justAddWater from './achievements/justAddWater';
|
||||
import lostMasterclasser from './achievements/lostMasterclasser';
|
||||
import mindOverMatter from './achievements/mindOverMatter';
|
||||
import loginIncentives from './achievements/login-incentives';
|
||||
import onboardingComplete from './achievements/onboardingComplete';
|
||||
import verifyUsername from './settings/verifyUsername';
|
||||
import firstDrops from './achievements/firstDrops';
|
||||
|
||||
const NOTIFICATIONS = {
|
||||
// general notifications
|
||||
NEW_CONTRIBUTOR_LEVEL: {
|
||||
achievement: true,
|
||||
label: $t => $t('modalContribAchievement'),
|
||||
modalId: 'contributor',
|
||||
sticky: true,
|
||||
},
|
||||
// achievement notifications
|
||||
ACHIEVEMENT: { // null data filled in handleUserNotifications
|
||||
achievement: true,
|
||||
modalId: 'generic-achievement',
|
||||
label: null,
|
||||
data: {
|
||||
message: $t => $t('achievement'),
|
||||
modalText: null,
|
||||
},
|
||||
},
|
||||
CHALLENGE_JOINED_ACHIEVEMENT: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('joinedChallenge')}`,
|
||||
modalId: 'joined-challenge',
|
||||
},
|
||||
ULTIMATE_GEAR_ACHIEVEMENT: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('gearAchievementNotification')}`,
|
||||
modalId: 'ultimate-gear',
|
||||
},
|
||||
GUILD_JOINED_ACHIEVEMENT: {
|
||||
label: $t => `${$t('achievement')}: ${$t('joinedGuild')}`,
|
||||
achievement: true,
|
||||
@@ -170,42 +176,14 @@ const NOTIFICATIONS = {
|
||||
label: $t => `${$t('achievement')}: ${$t('invitedFriend')}`,
|
||||
modalId: 'invited-friend',
|
||||
},
|
||||
NEW_CONTRIBUTOR_LEVEL: {
|
||||
ACHIEVEMENT_PARTY_ON: {
|
||||
achievement: true,
|
||||
label: $t => $t('modalContribAchievement'),
|
||||
modalId: 'contributor',
|
||||
sticky: true,
|
||||
},
|
||||
ACHIEVEMENT_ALL_YOUR_BASE: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementAllYourBase')}`,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementPartyOn')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'allYourBase', // defined manually until the server sends all the necessary data
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_BACK_TO_BASICS: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementBackToBasics')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'backToBasics',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_DUST_DEVIL: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementDustDevil')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'dustDevil',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_ARID_AUTHORITY: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementAridAuthority')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'aridAuthority',
|
||||
message: $t => $t('achievement'),
|
||||
modalText: $t => $t('achievementPartyOn'),
|
||||
achievement: 'partyOn',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_PARTY_UP: {
|
||||
@@ -218,245 +196,47 @@ const NOTIFICATIONS = {
|
||||
achievement: 'partyUp',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_PARTY_ON: {
|
||||
ULTIMATE_GEAR_ACHIEVEMENT: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('gearAchievementNotification')}`,
|
||||
modalId: 'ultimate-gear',
|
||||
},
|
||||
ACHIEVEMENT_STABLE: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementPartyOn')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
message: $t => $t('achievement'),
|
||||
modalText: $t => $t('achievementPartyOn'),
|
||||
achievement: 'partyOn',
|
||||
achievement: 'stableAchievs',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_BEAST_MASTER: {
|
||||
ACHIEVEMENT_QUESTS: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('beastAchievement')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
message: $t => $t('achievement'),
|
||||
modalText: $t => $t('beastAchievement'),
|
||||
achievement: 'beastMaster',
|
||||
achievement: 'questSeriesAchievs',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_MOUNT_MASTER: {
|
||||
ACHIEVEMENT_ANIMAL_SET: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('mountAchievement')}`,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementAnimalSet')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
message: $t => $t('achievement'),
|
||||
modalText: $t => $t('mountAchievement'),
|
||||
achievement: 'mountMaster',
|
||||
achievement: 'animalSetAchievs',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_TRIAD_BINGO: {
|
||||
ACHIEVEMENT_PET_COLOR: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('triadBingoAchievement')}`,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementPetColor')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
message: $t => $t('achievement'),
|
||||
modalText: $t => $t('triadBingoAchievement'),
|
||||
achievement: 'triadBingo',
|
||||
achievement: 'petColorAchievs',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_MONSTER_MAGUS: {
|
||||
ACHIEVEMENT_MOUNT_COLOR: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementMonsterMagus')}`,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementMountColor')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'monsterMagus',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_UNDEAD_UNDERTAKER: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementUndeadUndertaker')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'undeadUndertaker',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT: { // data filled in handleUserNotifications
|
||||
achievement: true,
|
||||
modalId: 'generic-achievement',
|
||||
label: null, // data filled in handleUserNotifications
|
||||
data: {
|
||||
message: $t => $t('achievement'),
|
||||
modalText: null, // data filled in handleUserNotifications
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_PRIMED_FOR_PAINTING: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementPrimedForPainting')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'primedForPainting',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_PEARLY_PRO: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementPearlyPro')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'pearlyPro',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_TICKLED_PINK: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementTickledPink')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'tickledPink',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_ROSY_OUTLOOK: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementRosyOutlook')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'rosyOutlook',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_BUG_BONANZA: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementBugBonanza')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'bugBonanza',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_BARE_NECESSITIES: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementBareNecessities')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'bareNecessities',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_FRESHWATER_FRIENDS: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementFreshwaterFriends')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'freshwaterFriends',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_GOOD_AS_GOLD: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementGoodAsGold')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'goodAsGold',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_ALL_THAT_GLITTERS: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementAllThatGlitters')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'allThatGlitters',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_BONE_COLLECTOR: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementBoneCollector')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'boneCollector',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_SKELETON_CREW: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementSkeletonCrew')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'skeletonCrew',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_SEEING_RED: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementSeeingRed')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'seeingRed',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_RED_LETTER_DAY: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementRedLetterDay')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'redLetterDay',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_LEGENDARY_BESTIARY: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementLegendaryBestiary')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'legendaryBestiary',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_SEASONAL_SPECIALIST: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementSeasonalSpecialist')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'seasonalSpecialist',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_VIOLETS_ARE_BLUE: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementVioletsAreBlue')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'violetsAreBlue',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_WILD_BLUE_YONDER: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementWildBlueYonder')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'wildBlueYonder',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_DOMESTICATED: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementDomesticated')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'domesticated',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_SHADY_CUSTOMER: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementShadyCustomer')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'shadyCustomer',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_SHADE_OF_IT_ALL: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementShadeOfItAll')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'shadeOfItAll',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_ZODIAC_ZOOKEEPER: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementZodiacZookeeper')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'zodiacZookeeper',
|
||||
},
|
||||
},
|
||||
ACHIEVEMENT_BIRDS_OF_A_FEATHER: {
|
||||
achievement: true,
|
||||
label: $t => `${$t('achievement')}: ${$t('achievementBirdsOfAFeather')}`,
|
||||
modalId: 'generic-achievement',
|
||||
data: {
|
||||
achievement: 'birdsOfAFeather',
|
||||
achievement: 'mountColorAchievs',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -486,9 +266,6 @@ export default {
|
||||
loginIncentives,
|
||||
verifyUsername,
|
||||
genericAchievement,
|
||||
lostMasterclasser,
|
||||
mindOverMatter,
|
||||
justAddWater,
|
||||
onboardingComplete,
|
||||
firstDrops,
|
||||
},
|
||||
@@ -509,21 +286,30 @@ export default {
|
||||
const handledNotifications = {};
|
||||
|
||||
[
|
||||
'GUILD_PROMPT', 'REBIRTH_ENABLED', 'WON_CHALLENGE', 'STREAK_ACHIEVEMENT',
|
||||
'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT',
|
||||
'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL',
|
||||
'CRON', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS',
|
||||
'GENERIC_ACHIEVEMENT', 'ACHIEVEMENT_PARTY_UP', 'ACHIEVEMENT_PARTY_ON', 'ACHIEVEMENT_BEAST_MASTER',
|
||||
'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY',
|
||||
'ACHIEVEMENT_MONSTER_MAGUS', 'ACHIEVEMENT_UNDEAD_UNDERTAKER', 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
|
||||
'ACHIEVEMENT_PEARLY_PRO', 'ACHIEVEMENT_TICKLED_PINK', 'ACHIEVEMENT_ROSY_OUTLOOK', 'ACHIEVEMENT',
|
||||
'ONBOARDING_COMPLETE', 'FIRST_DROPS', 'ACHIEVEMENT_BUG_BONANZA', 'ACHIEVEMENT_BARE_NECESSITIES',
|
||||
'ACHIEVEMENT_FRESHWATER_FRIENDS', 'ACHIEVEMENT_GOOD_AS_GOLD', 'ACHIEVEMENT_ALL_THAT_GLITTERS',
|
||||
'ACHIEVEMENT_BONE_COLLECTOR', 'ACHIEVEMENT_SKELETON_CREW', 'ACHIEVEMENT_SEEING_RED',
|
||||
'ACHIEVEMENT_RED_LETTER_DAY', 'ACHIEVEMENT_LEGENDARY_BESTIARY', 'ACHIEVEMENT_SEASONAL_SPECIALIST',
|
||||
'ACHIEVEMENT_VIOLETS_ARE_BLUE', 'ACHIEVEMENT_WILD_BLUE_YONDER', 'ACHIEVEMENT_DOMESTICATED',
|
||||
'ACHIEVEMENT_SHADY_CUSTOMER', 'ACHIEVEMENT_SHADE_OF_IT_ALL', 'ACHIEVEMENT_ZODIAC_ZOOKEEPER',
|
||||
'ACHIEVEMENT_BIRDS_OF_A_FEATHER',
|
||||
// general notifications
|
||||
'CRON',
|
||||
'FIRST_DROPS',
|
||||
'GUILD_PROMPT',
|
||||
'LOGIN_INCENTIVE',
|
||||
'NEW_CONTRIBUTOR_LEVEL',
|
||||
'ONBOARDING_COMPLETE',
|
||||
'REBIRTH_ENABLED',
|
||||
'WON_CHALLENGE',
|
||||
// achievement notifications
|
||||
'ACHIEVEMENT',
|
||||
'CHALLENGE_JOINED_ACHIEVEMENT',
|
||||
'GUILD_JOINED_ACHIEVEMENT',
|
||||
'INVITED_FRIEND_ACHIEVEMENT',
|
||||
'ACHIEVEMENT_PARTY_ON',
|
||||
'ACHIEVEMENT_PARTY_UP',
|
||||
'REBIRTH_ACHIEVEMENT',
|
||||
'STREAK_ACHIEVEMENT',
|
||||
'ULTIMATE_GEAR_ACHIEVEMENT',
|
||||
'ACHIEVEMENT_STABLE',
|
||||
'ACHIEVEMENT_QUESTS',
|
||||
'ACHIEVEMENT_ANIMAL_SET',
|
||||
'ACHIEVEMENT_PET_COLOR',
|
||||
'ACHIEVEMENT_MOUNT_COLOR',
|
||||
].forEach(type => {
|
||||
handledNotifications[type] = true;
|
||||
});
|
||||
@@ -921,57 +707,68 @@ export default {
|
||||
case 'WON_CHALLENGE':
|
||||
this.$root.$emit('habitica:won-challenge', notification);
|
||||
break;
|
||||
case 'REBIRTH_ACHIEVEMENT':
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.$root.$emit('bv::show::modal', 'rebirth');
|
||||
break;
|
||||
case 'STREAK_ACHIEVEMENT':
|
||||
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {
|
||||
this.$root.$emit('bv::show::modal', 'streak');
|
||||
}, this.user.preferences.suppressModals.streak);
|
||||
this.playSound('Achievement_Unlocked');
|
||||
break;
|
||||
case 'REBIRTH_ACHIEVEMENT':
|
||||
this.playSound('Achievement_Unlocked');
|
||||
this.$root.$emit('bv::show::modal', 'rebirth');
|
||||
break;
|
||||
case 'ULTIMATE_GEAR_ACHIEVEMENT':
|
||||
case 'GUILD_JOINED_ACHIEVEMENT':
|
||||
case 'CHALLENGE_JOINED_ACHIEVEMENT':
|
||||
case 'INVITED_FRIEND_ACHIEVEMENT':
|
||||
case 'NEW_CONTRIBUTOR_LEVEL':
|
||||
case 'ACHIEVEMENT_ALL_YOUR_BASE':
|
||||
case 'ACHIEVEMENT_BACK_TO_BASICS':
|
||||
case 'ACHIEVEMENT_DUST_DEVIL':
|
||||
case 'ACHIEVEMENT_ARID_AUTHORITY':
|
||||
case 'ACHIEVEMENT_PARTY_UP':
|
||||
case 'CHALLENGE_JOINED_ACHIEVEMENT':
|
||||
case 'GUILD_JOINED_ACHIEVEMENT':
|
||||
case 'INVITED_FRIEND_ACHIEVEMENT':
|
||||
case 'ACHIEVEMENT_PARTY_ON':
|
||||
case 'ACHIEVEMENT_BEAST_MASTER':
|
||||
case 'ACHIEVEMENT_MOUNT_MASTER':
|
||||
case 'ACHIEVEMENT_TRIAD_BINGO':
|
||||
case 'ACHIEVEMENT_MONSTER_MAGUS':
|
||||
case 'ACHIEVEMENT_UNDEAD_UNDERTAKER':
|
||||
case 'ACHIEVEMENT_PRIMED_FOR_PAINTING':
|
||||
case 'ACHIEVEMENT_PEARLY_PRO':
|
||||
case 'ACHIEVEMENT_TICKLED_PINK':
|
||||
case 'ACHIEVEMENT_ROSY_OUTLOOK':
|
||||
case 'ACHIEVEMENT_BUG_BONANZA':
|
||||
case 'ACHIEVEMENT_BARE_NECESSITIES':
|
||||
case 'ACHIEVEMENT_FRESHWATER_FRIENDS':
|
||||
case 'ACHIEVEMENT_GOOD_AS_GOLD':
|
||||
case 'ACHIEVEMENT_ALL_THAT_GLITTERS':
|
||||
case 'ACHIEVEMENT_BONE_COLLECTOR':
|
||||
case 'ACHIEVEMENT_SKELETON_CREW':
|
||||
case 'ACHIEVEMENT_SEEING_RED':
|
||||
case 'ACHIEVEMENT_RED_LETTER_DAY':
|
||||
case 'ACHIEVEMENT_LEGENDARY_BESTIARY':
|
||||
case 'ACHIEVEMENT_SEASONAL_SPECIALIST':
|
||||
case 'ACHIEVEMENT_VIOLETS_ARE_BLUE':
|
||||
case 'ACHIEVEMENT_WILD_BLUE_YONDER':
|
||||
case 'ACHIEVEMENT_DOMESTICATED':
|
||||
case 'ACHIEVEMENT_SHADY_CUSTOMER':
|
||||
case 'ACHIEVEMENT_SHADE_OF_IT_ALL':
|
||||
case 'ACHIEVEMENT_ZODIAC_ZOOKEEPER':
|
||||
case 'ACHIEVEMENT_BIRDS_OF_A_FEATHER':
|
||||
case 'GENERIC_ACHIEVEMENT':
|
||||
case 'ACHIEVEMENT_PARTY_UP':
|
||||
case 'ULTIMATE_GEAR_ACHIEVEMENT':
|
||||
this.showNotificationWithModal(notification);
|
||||
break;
|
||||
case 'ACHIEVEMENT_QUESTS': {
|
||||
const { achievement } = notification.data;
|
||||
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
|
||||
const achievementTitleKey = `achievement${upperCaseAchievement}`;
|
||||
NOTIFICATIONS.ACHIEVEMENT_QUESTS.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
|
||||
this.showNotificationWithModal(notification);
|
||||
Vue.set(this.user.achievements, achievement, true);
|
||||
break;
|
||||
}
|
||||
case 'ACHIEVEMENT_STABLE': {
|
||||
const { achievement, achievementNotification } = notification.data;
|
||||
NOTIFICATIONS.ACHIEVEMENT_STABLE.label = $t => `${$t('achievement')}: ${$t(achievementNotification)}`;
|
||||
this.showNotificationWithModal(notification);
|
||||
Vue.set(this.user.achievements, achievement, true);
|
||||
break;
|
||||
}
|
||||
case 'ACHIEVEMENT_ANIMAL_SET': {
|
||||
const { achievement } = notification.data;
|
||||
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
|
||||
const achievementTitleKey = `achievement${upperCaseAchievement}`;
|
||||
NOTIFICATIONS.ACHIEVEMENT_ANIMAL_SET.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
|
||||
this.showNotificationWithModal(notification);
|
||||
Vue.set(this.user.achievements, achievement, true);
|
||||
break;
|
||||
}
|
||||
case 'ACHIEVEMENT_PET_COLOR': {
|
||||
const { achievement } = notification.data;
|
||||
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
|
||||
const achievementTitleKey = `achievement${upperCaseAchievement}`;
|
||||
NOTIFICATIONS.ACHIEVEMENT_PET_COLOR.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
|
||||
this.showNotificationWithModal(notification);
|
||||
Vue.set(this.user.achievements, achievement, true);
|
||||
break;
|
||||
}
|
||||
case 'ACHIEVEMENT_MOUNT_COLOR': {
|
||||
const { achievement } = notification.data;
|
||||
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
|
||||
const achievementTitleKey = `achievement${upperCaseAchievement}`;
|
||||
NOTIFICATIONS.ACHIEVEMENT_MOUNT_COLOR.label = $t => `${$t('achievement')}: ${$t(achievementTitleKey)}`;
|
||||
this.showNotificationWithModal(notification);
|
||||
Vue.set(this.user.achievements, achievement, true);
|
||||
break;
|
||||
}
|
||||
case 'ACHIEVEMENT': { // generic achievement
|
||||
const { achievement } = notification.data;
|
||||
const upperCaseAchievement = achievement.charAt(0).toUpperCase() + achievement.slice(1);
|
||||
@@ -984,10 +781,6 @@ export default {
|
||||
Vue.set(this.user.achievements, achievement, true);
|
||||
break;
|
||||
}
|
||||
case 'CRON':
|
||||
// Not needed because it's shown already by the userHp and userMp watchers
|
||||
// Keeping an empty block so that it gets read
|
||||
break;
|
||||
case 'LOGIN_INCENTIVE':
|
||||
if (this.user.flags.tour.intro === this.TOUR_END && this.user.flags.welcomed) {
|
||||
this.notificationData = notification.data;
|
||||
|
||||
@@ -198,7 +198,7 @@ export default {
|
||||
appState.newGroup = false;
|
||||
appState.group = pick(this.amazonPayments.group, ['_id', 'memberCount', 'name']);
|
||||
}
|
||||
} else if (paymentType.indexOf('gift-') === 0) {
|
||||
} else if (paymentType && paymentType.indexOf('gift-') === 0) {
|
||||
appState.gift = this.amazonPayments.gift;
|
||||
appState.giftReceiver = this.amazonPayments.giftReceiver;
|
||||
} else if (paymentType === 'gems') {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="payments-column mx-auto mt-auto">
|
||||
<h4>{{ $t('choosePaymentMethod') }}</h4>
|
||||
<button
|
||||
v-if="stripeAvailable"
|
||||
class="btn btn-primary payment-button payment-item with-icon"
|
||||
@@ -80,6 +81,13 @@
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -14,15 +14,44 @@
|
||||
</div>
|
||||
<h2
|
||||
v-else
|
||||
class="ml-2"
|
||||
class="d-flex flex-column mx-auto align-items-center"
|
||||
>
|
||||
{{ $t('sendGift') }}
|
||||
{{ $t('sendAGift') }}
|
||||
</h2>
|
||||
<div
|
||||
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>
|
||||
<div
|
||||
v-else
|
||||
class="d-flex flex-column align-items-center">
|
||||
<div
|
||||
class="svg-big-gift"
|
||||
v-once
|
||||
v-html="icons.bigGift"
|
||||
></div>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<div
|
||||
class="modal-close"
|
||||
v-if="currentEvent && currentEvent.promo === 'g1g1'"
|
||||
class="g1g1-modal-close"
|
||||
@click="close()"
|
||||
>
|
||||
<div
|
||||
class="g1g1-svg-icon"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="modal-close"
|
||||
@click="close()">
|
||||
<div
|
||||
class="svg-icon"
|
||||
v-html="icons.close"
|
||||
@@ -42,6 +71,7 @@
|
||||
v-model="userSearchTerm"
|
||||
class="form-control"
|
||||
type="text"
|
||||
ref="textBox"
|
||||
:placeholder="$t('usernameOrUserId')"
|
||||
:class="{
|
||||
'input-valid': foundUser._id,
|
||||
@@ -70,15 +100,20 @@
|
||||
<div
|
||||
v-else
|
||||
>
|
||||
{{ $t('selectGift') }}
|
||||
{{ $t('next') }}
|
||||
</div>
|
||||
</button>
|
||||
<a
|
||||
class="cancel-link mx-auto mt-3"
|
||||
@click="close()"
|
||||
<div
|
||||
v-if="currentEvent && currentEvent.promo ==='g1g1'"
|
||||
class="g1g1-cancel d-flex justify-content-center"
|
||||
v-html="$t('cancel')"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('cancel') }}
|
||||
</a>
|
||||
{{ $t('cancel') }}
|
||||
</div>
|
||||
<div
|
||||
v-else>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,13 +145,16 @@
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
|
||||
#select-user-modal {
|
||||
.modal-content {
|
||||
width:448px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: 0rem;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 29.5rem;
|
||||
margin-top: 25vh;
|
||||
width: 448px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
@@ -126,7 +164,16 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -146,12 +193,12 @@
|
||||
|
||||
.g1g1 {
|
||||
background-image: url('~@/assets/images/g1g1-send.png');
|
||||
background-size: 472px 152px;
|
||||
width: 470px;
|
||||
background-size: 446px 152px;
|
||||
width: 446px;
|
||||
height: 152px;
|
||||
margin: -1rem 0rem 0rem -1rem;
|
||||
border-radius: 0.3rem 0.3rem 0rem 0rem;
|
||||
padding: 1.5rem;
|
||||
margin: -16px 0px 0px -16px;
|
||||
border-radius: 4.8px 4.8px 0px 0px;
|
||||
padding: 24px;
|
||||
color: $white;
|
||||
|
||||
h1 {
|
||||
@@ -169,6 +216,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -176,6 +233,29 @@
|
||||
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%;
|
||||
@@ -192,6 +272,18 @@
|
||||
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;
|
||||
@@ -206,14 +298,17 @@
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// import { nextTick } from 'vue'; // may not need this? I don't know!
|
||||
import debounce from 'lodash/debounce';
|
||||
import find from 'lodash/find';
|
||||
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';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
@@ -223,6 +318,7 @@ export default {
|
||||
foundUser: {},
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
bigGift: bigGiftIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
@@ -281,7 +377,7 @@ export default {
|
||||
this.foundUser = result;
|
||||
}, 500),
|
||||
selectUser () {
|
||||
this.$root.$emit('habitica::send-gems', this.foundUser);
|
||||
this.$root.$emit('habitica::send-gift', this.foundUser);
|
||||
this.close();
|
||||
},
|
||||
onHide () {
|
||||
|
||||
@@ -0,0 +1,659 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="send-gift"
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
size="md"
|
||||
@hide="onHide()"
|
||||
>
|
||||
<div>
|
||||
<!-- header -->
|
||||
<div
|
||||
class="modal-close"
|
||||
@click="close()"
|
||||
>
|
||||
<div
|
||||
class="icon-close"
|
||||
v-html="icons.closeIcon"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="d-flex flex-column mx-auto align-items-center">
|
||||
{{ $t('sendAGift') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- user avatar -->
|
||||
<div
|
||||
v-if="userReceivingGift"
|
||||
class="modal-body"
|
||||
>
|
||||
<avatar
|
||||
:member="userReceivingGift"
|
||||
:hideClassBadge="true"
|
||||
class="d-flex flex-column mx-auto align-items-center"
|
||||
/>
|
||||
<div class="avatar-spacer"></div>
|
||||
<div class="d-flex flex-column mx-auto align-items-center display-name">
|
||||
<!-- user display name and username -->
|
||||
<user-link
|
||||
:user-id="displayName"
|
||||
:name="displayName"
|
||||
:backer="userBacker"
|
||||
:contributor="userContributor"
|
||||
:class="display-name"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex flex-column mx-auto align-items-center user-name">
|
||||
@{{ userName }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- menu area -->
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2 text-center nav">
|
||||
<div
|
||||
class="nav-link"
|
||||
:class="{active: selectedPage === 'subscription'}"
|
||||
@click="selectPage('subscription')"
|
||||
>
|
||||
{{ $t('subscription') }}
|
||||
</div>
|
||||
<div
|
||||
class="nav-link"
|
||||
:class="{active: selectedPage !== 'subscription'}"
|
||||
@click="selectPage('buyGems')"
|
||||
>
|
||||
{{ $t('gems') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- subscriber block -->
|
||||
<subscription-options
|
||||
v-show="selectedPage === 'subscription'"
|
||||
class="subscribe-option"
|
||||
:userReceivingGift="userReceivingGift"
|
||||
/>
|
||||
|
||||
<!-- gem block -->
|
||||
<div
|
||||
v-show="selectedPage === 'buyGems'"
|
||||
>
|
||||
<div class="gem-group">
|
||||
<!-- buy gems with money -->
|
||||
<label v-once>
|
||||
{{ $t('howManyGemsPurchase') }}
|
||||
</label>
|
||||
<div class="d-flex flex-row align-items-center justify-content-center">
|
||||
<div
|
||||
class="gray-circle"
|
||||
@click="gift.gems.amount <= 0
|
||||
? gift.gems.amount = 0
|
||||
: gift.gems.amount--"
|
||||
>
|
||||
<div
|
||||
class="icon-negative"
|
||||
v-html="icons.negativeIcon"
|
||||
></div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-icon align-items-center">
|
||||
<div
|
||||
class="icon-gem"
|
||||
v-html="icons.gemIcon"
|
||||
></div>
|
||||
</div>
|
||||
<input
|
||||
id="gemsForm"
|
||||
v-model.number="gift.gems.amount"
|
||||
class="form-control"
|
||||
max="9999"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="gray-circle"
|
||||
@click="gift.gems.amount++"
|
||||
>
|
||||
<div
|
||||
class="icon-positive"
|
||||
v-html="icons.positiveIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- the word "total" -->
|
||||
<div class="buy-gem-total">
|
||||
{{ $t('sendGiftTotal') }}
|
||||
</div>
|
||||
|
||||
<!-- the actual dollar amount -->
|
||||
<div class="buy-gem-amount">
|
||||
<span>
|
||||
{{ formatter.format(totalGems) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- change to sending own gems page -->
|
||||
<div
|
||||
:class="{active: selectedPage === 'ownGems'}"
|
||||
class="gem-state-change"
|
||||
@click="selectPage('ownGems')"
|
||||
>
|
||||
{{ $t('wantToSendOwnGems') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- paying for gems -->
|
||||
<payments-buttons
|
||||
class="payment-buttons"
|
||||
:stripe-fn="() => redirectToStripe({gift, uuid: userReceivingGift._id, receiverName})"
|
||||
:paypal-fn="() => openPaypalGift({
|
||||
gift: gift, giftedTo: userReceivingGift._id, receiverName,
|
||||
})"
|
||||
:amazon-data="{type: 'single', gift, giftedTo: userReceivingGift._id, receiverName}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- send gems from balance -->
|
||||
<div
|
||||
v-show="selectedPage === 'ownGems'"
|
||||
>
|
||||
<div class="gem-group">
|
||||
<label v-once>
|
||||
{{ $t('howManyGemsSend') }}
|
||||
</label>
|
||||
<div class="d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
class="gray-circle"
|
||||
@click="gift.gems.amount <= 0
|
||||
? gift.gems.amount = 0
|
||||
: gift.gems.amount--"
|
||||
>
|
||||
<div
|
||||
class="icon-negative"
|
||||
v-html="icons.negativeIcon"
|
||||
></div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend input-group-icon align-items-center">
|
||||
<div
|
||||
class="icon-gem"
|
||||
v-html="icons.gemIcon"
|
||||
></div>
|
||||
</div>
|
||||
<input
|
||||
id="gemsForm"
|
||||
v-model="gift.gems.amount"
|
||||
class="form-control"
|
||||
:max="maxGems"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="gray-circle"
|
||||
@click="gift.gems.amount < maxGems
|
||||
? gift.gems.amount++
|
||||
: gift.gems.amount = maxGems"
|
||||
>
|
||||
<div
|
||||
class="icon-positive"
|
||||
v-html="icons.positiveIcon"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-items-middle">
|
||||
<div class="d-flex justify-content-center align-items-middle">
|
||||
<span class="balance-text">
|
||||
{{ $t('yourBalance') }}
|
||||
</span>
|
||||
<span
|
||||
class="icon-gem balance-gem-margin"
|
||||
style="display: inline-block;"
|
||||
v-html="icons.gemIcon"
|
||||
></span>
|
||||
<span
|
||||
class="balance-gems"
|
||||
>
|
||||
{{ maxGems }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column justify-content-center align-items-middle mt-3">
|
||||
<button
|
||||
v-if="fromBal"
|
||||
class="btn btn-primary mx-auto mt-2"
|
||||
type="submit"
|
||||
:disabled="sendingInProgress"
|
||||
@click="sendGift()"
|
||||
>
|
||||
{{ $t("send") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- change to buying gems page -->
|
||||
<div
|
||||
:class="{active: selectedPage === 'buyGems'}"
|
||||
class="gem-state-change"
|
||||
@click="selectPage('buyGems')"
|
||||
>
|
||||
{{ $t('needToPurchaseGems') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
#send-gift {
|
||||
.modal-dialog {
|
||||
max-width: 448px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 448px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
|
||||
}
|
||||
.modal-body{
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-close {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
|
||||
& ::v-deep svg path {
|
||||
fill: #878190;
|
||||
}
|
||||
& :hover {
|
||||
fill: #686274;
|
||||
}
|
||||
}
|
||||
}
|
||||
#subscription-form .subscribe-option {
|
||||
background: #F9F9F9;
|
||||
}
|
||||
|
||||
#subscription-form .selected {
|
||||
background: rgba(213, 200, 255, 0.32);
|
||||
// using rgba for transparency
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
h2 {
|
||||
color: $purple-300;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.avatar-spacer {
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
margin: 0px 6px 0 20px;
|
||||
}
|
||||
|
||||
.display-name a:hover{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
text-align: center;
|
||||
color: $gray-100;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.row {
|
||||
background-color: $gray-700;
|
||||
margin: 0 0 0 0;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
font-weight: bold;
|
||||
font-size: 0.75rem;
|
||||
min-height: 32px;
|
||||
padding: 16px 0 0 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: $gray-100;
|
||||
display: inline-block;
|
||||
padding: 0px 8px 6px 8px;
|
||||
|
||||
&.active {
|
||||
color: $purple-300;
|
||||
border-bottom: 2px solid $purple-400;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $purple-300;
|
||||
border-bottom: 2px solid $purple-400;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-group {
|
||||
padding: 0 0 24px 0;
|
||||
background-color: $gray-700;
|
||||
margin: 0 0 0 0;
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px
|
||||
}
|
||||
|
||||
label {
|
||||
color: $gray-50;
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
margin: 12px 0 16px 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
width: 94px;
|
||||
height: 32px;
|
||||
margin: 0px 16px 0px 16px;
|
||||
padding: 0;
|
||||
border-radius: 2px;
|
||||
border: solid 1px $gray-400;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.gray-circle {
|
||||
border-radius: 100%;
|
||||
border: solid 2px $gray-300;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
}
|
||||
|
||||
.gray-circle:hover{
|
||||
.icon-positive, .icon-negative {
|
||||
& ::v-deep svg path {
|
||||
fill: $purple-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon-gem {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.icon-positive, .icon-negative {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin: 4px auto;
|
||||
|
||||
& ::v-deep svg path {
|
||||
fill: $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
.buy-gem-total {
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
padding-top: 24px;
|
||||
text-align: center;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.buy-gem-amount {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin: 16px 0 24px 0;
|
||||
text-align: center;
|
||||
height: 28px;
|
||||
color: $green-10;
|
||||
}
|
||||
|
||||
.balance-text {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: $gray-100;
|
||||
line-height: 1.33;
|
||||
margin: 12px 0px 0px 70px;
|
||||
}
|
||||
|
||||
.balance-gem-margin {
|
||||
margin: 8px 4px 0px 8px;
|
||||
}
|
||||
|
||||
.balance-gems {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-100;
|
||||
line-height: 1.33;
|
||||
margin: 12px 71px 0px 4px;
|
||||
}
|
||||
|
||||
.gem-state-change {
|
||||
color: $blue-10;
|
||||
font-size: 0.875rem;
|
||||
min-height: 24px;
|
||||
margin: 16px 0 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subscribe-option {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.payment-buttons {
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
// libs imports
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
// mixins imports
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
|
||||
// component imports
|
||||
import avatar from '../avatar';
|
||||
import userLink from '../userLink';
|
||||
import subscriptionOptions from '../settings/subscriptionOptions.vue';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
|
||||
// svg imports
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
import gemIcon from '@/assets/svg/gem.svg';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import negativeIcon from '@/assets/svg/negative.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
avatar,
|
||||
subscriptionOptions,
|
||||
paymentsButtons,
|
||||
userLink,
|
||||
},
|
||||
mixins: [
|
||||
paymentsMixin,
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
subscription: {
|
||||
key: '',
|
||||
},
|
||||
icons: Object.freeze({
|
||||
closeIcon,
|
||||
gemIcon,
|
||||
positiveIcon,
|
||||
negativeIcon,
|
||||
}),
|
||||
userReceivingGift: {
|
||||
profile: '',
|
||||
},
|
||||
name: '',
|
||||
display: '',
|
||||
selectedPage: 'subscription',
|
||||
gift: {
|
||||
type: 'gems',
|
||||
gems: {
|
||||
amount: 0,
|
||||
fromBalance: true,
|
||||
},
|
||||
},
|
||||
sendingInProgress: false,
|
||||
amazonPayments: {},
|
||||
gemCost: 1,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
userLoggedIn: 'user.data',
|
||||
}),
|
||||
userName () {
|
||||
const userName = this.userReceivingGift.auth
|
||||
&& this.userReceivingGift.auth.local
|
||||
&& this.userReceivingGift.auth.local.username;
|
||||
return userName;
|
||||
},
|
||||
displayName () {
|
||||
const displayName = this.userReceivingGift.profile.name;
|
||||
return displayName;
|
||||
},
|
||||
userBacker () {
|
||||
const userBacker = this.userReceivingGift.backer;
|
||||
return userBacker;
|
||||
},
|
||||
userContributor () {
|
||||
const userContributor = this.userReceivingGift.contributor;
|
||||
return userContributor;
|
||||
},
|
||||
tierIcon () {
|
||||
if (this.isNPC) {
|
||||
return this.icons.tierNPC;
|
||||
}
|
||||
return this.icons[`tier${this.level}`];
|
||||
},
|
||||
fromBal () {
|
||||
return this.gift.type === 'gems' && this.gift.gems.fromBalance;
|
||||
},
|
||||
maxGems () {
|
||||
const maxGems = this.fromBal ? this.userLoggedIn.balance * 4 : 9999;
|
||||
return maxGems;
|
||||
},
|
||||
formatter () {
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
minimumFractionDigits: 2,
|
||||
});
|
||||
return formatter;
|
||||
},
|
||||
totalGems () {
|
||||
const totalGems = this.gift.gems.amount * 0.25;
|
||||
return totalGems;
|
||||
},
|
||||
receiverName () {
|
||||
if (
|
||||
this.userReceivingGift.auth
|
||||
&& this.userReceivingGift.auth.local
|
||||
&& this.userReceivingGift.auth.local.username
|
||||
) {
|
||||
return this.userReceivingGift.auth.local.username;
|
||||
}
|
||||
return this.userReceivingGift.profile.name;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
startingPage () {
|
||||
this.selectedPage = this.startingPage;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica::send-gift', data => {
|
||||
this.userReceivingGift = data;
|
||||
if (this.$store.state.giftModalOptions.startingPage) {
|
||||
this.selectedPage = this.$store.state.giftModalOptions.startingPage;
|
||||
this.$store.state.giftModalOptions.startingPage = '';
|
||||
this.selectPage(this.selectedPage);
|
||||
} else {
|
||||
this.selectPage(this.startingPage);
|
||||
}
|
||||
this.setGemDefaults();
|
||||
this.$root.$emit('bv::show::modal', 'send-gift');
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'send-gift');
|
||||
},
|
||||
setGemDefaults () {
|
||||
if (this.selectedPage === 'buyGems') {
|
||||
this.gift.gems.amount = 20;
|
||||
} else if (this.selectedPage === 'ownGems') {
|
||||
this.gift.gems.amount = 1;
|
||||
} else {
|
||||
this.gift.gems.amount = 0;
|
||||
}
|
||||
},
|
||||
selectPage (page) {
|
||||
if (page === this.selectedPage) return;
|
||||
if (page === 'buyGems') this.selectedPage = 'buyGems';
|
||||
if (page === 'buyGems' && this.selectedPage === 'ownGems') return;
|
||||
this.selectedPage = page || 'subscription';
|
||||
this.setGemDefaults();
|
||||
},
|
||||
async sendGift () {
|
||||
this.sendingInProgress = true;
|
||||
await this.$store.dispatch('members:transferGems', {
|
||||
toUserId: this.userReceivingGift._id,
|
||||
gemAmount: this.gift.gems.amount,
|
||||
});
|
||||
this.close();
|
||||
setTimeout(() => { // wait for the send gem modal to be closed
|
||||
this.$root.$emit('habitica:payment-success', {
|
||||
paymentMethod: 'balance',
|
||||
paymentCompleted: true,
|
||||
paymentType: 'gift-gems-balance',
|
||||
gift: {
|
||||
gems: {
|
||||
amount: this.gift.gems.amount,
|
||||
},
|
||||
},
|
||||
giftReceiver: this.receiverName,
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
onHide () {
|
||||
this.sendingInProgress = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -739,6 +739,14 @@ export default {
|
||||
} 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) {
|
||||
|
||||
@@ -450,10 +450,6 @@
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.subscribe-option {
|
||||
border-bottom: 1px solid $gray-600;
|
||||
}
|
||||
|
||||
.svg-amazon-pay {
|
||||
width: 208px;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,17 @@
|
||||
:value="block.key"
|
||||
class="subscribe-option pt-2 pl-5 pb-3 mb-0"
|
||||
:class="{selected: subscription.key === block.key}"
|
||||
@click.native="subscription.key = block.key"
|
||||
@click.native="updateSubscriptionData(block.key)"
|
||||
>
|
||||
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
|
||||
<div
|
||||
v-if="userReceivingGift && userReceivingGift._id"
|
||||
class="subscription-text ml-2 mb-1"
|
||||
v-html="$t('giftSubscriptionRateText', {price: block.price, months: block.months})"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="subscription-text ml-2 mb-1"
|
||||
v-html="$t('subscriptionRateText', {price: block.price, months: block.months})"
|
||||
>
|
||||
@@ -25,7 +32,18 @@
|
||||
</div>
|
||||
</b-form-radio>
|
||||
</b-form-group>
|
||||
<!-- payment buttons first is for gift subs and the second is for renewing subs -->
|
||||
<payments-buttons
|
||||
v-if="userReceivingGift && userReceivingGift._id"
|
||||
:disabled="!subscription.key"
|
||||
:stripe-fn="() => redirectToStripe({gift, uuid: userReceivingGift._id, receiverName})"
|
||||
:paypal-fn="() => openPaypalGift({
|
||||
gift: gift, giftedTo: userReceivingGift._id, receiverName,
|
||||
})"
|
||||
:amazon-data="{type: 'single', gift, giftedTo: userReceivingGift._id, receiverName}"
|
||||
/>
|
||||
<payments-buttons
|
||||
v-else
|
||||
:disabled="!subscription.key"
|
||||
:stripe-fn="() => redirectToStripe({
|
||||
subscription: subscription.key,
|
||||
@@ -43,6 +61,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#subscription-form {
|
||||
.custom-control .custom-control-label::before,
|
||||
.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
|
||||
@@ -101,11 +120,22 @@ export default {
|
||||
mixins: [
|
||||
paymentsMixin,
|
||||
],
|
||||
props: {
|
||||
userReceivingGift: {
|
||||
type: Object,
|
||||
default () {},
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
subscription: {
|
||||
key: null,
|
||||
key: 'basic_earned',
|
||||
},
|
||||
gift: {
|
||||
type: 'subscription',
|
||||
subscription: { key: 'basic_earned' },
|
||||
},
|
||||
receiverName: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -114,7 +144,6 @@ export default {
|
||||
},
|
||||
subscriptionBlocksOrdered () {
|
||||
const subscriptions = filter(subscriptionBlocks, o => o.discount !== true);
|
||||
|
||||
return sortBy(subscriptions, [o => o.months]);
|
||||
},
|
||||
},
|
||||
@@ -131,6 +160,10 @@ export default {
|
||||
return '<span class="subscription-bubble px-2 py-1">Gem cap at 25</span>';
|
||||
}
|
||||
},
|
||||
updateSubscriptionData (key) {
|
||||
this.subscription.key = key;
|
||||
if (this.userReceivingGift._id) this.gift.subscription.key = key;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -89,9 +89,6 @@ export default {
|
||||
try {
|
||||
this.$store.dispatch('shops:releasePets', { user: this.user });
|
||||
this.text(this.$t('releasePetsSuccess'));
|
||||
// this.$router.push({name: 'stable'});
|
||||
// Reload because achievement is set in user.save instead of common
|
||||
window.location.reload(true);
|
||||
} catch (err) {
|
||||
window.alert(err.message); // eslint-disable-line no-alert
|
||||
}
|
||||
@@ -112,9 +109,6 @@ export default {
|
||||
try {
|
||||
this.$store.dispatch('shops:releaseMounts', { user: this.user });
|
||||
this.text(this.$t('releaseMountsSuccess'));
|
||||
// this.$router.push({name: 'stable'});
|
||||
// Reload because achievement is set in user.save instead of common
|
||||
window.location.reload(true);
|
||||
} catch (err) {
|
||||
window.alert(err.message); // eslint-disable-line no-alert
|
||||
}
|
||||
@@ -135,9 +129,6 @@ export default {
|
||||
try {
|
||||
this.$store.dispatch('shops:releaseBoth', { user: this.user });
|
||||
this.text(this.$t('releaseBothSuccess'));
|
||||
// this.$router.push({name: 'stable'});
|
||||
// Reload because achievement is set in user.save instead of common
|
||||
window.location.reload(true);
|
||||
} catch (err) {
|
||||
window.alert(err.message); // eslint-disable-line no-alert
|
||||
}
|
||||
|
||||
@@ -560,14 +560,8 @@ export default {
|
||||
this.selectedItemToBuy = null;
|
||||
}
|
||||
},
|
||||
isGearLocked (gear) {
|
||||
if (gear.value > this.userStats.gp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
selectItem (item) {
|
||||
if (item.locked) return;
|
||||
this.selectedItemToBuy = item;
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'buy-quest-modal');
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
width: 350px;
|
||||
z-index: 999;
|
||||
z-index: 9999; // to keep it above modal overlays
|
||||
|
||||
top: var(--current-scrollY);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</a>
|
||||
<a
|
||||
href="https://geo.itunes.apple.com/us/app/habitica/id994882113?mt=8"
|
||||
style="display:inline-block;overflow:hidden;background:url(http://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"
|
||||
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>
|
||||
|
||||
@@ -209,6 +209,7 @@
|
||||
<li>Fox_town</li>
|
||||
<li>MaybeSteveRogers</li>
|
||||
<li>shanaqui</li>
|
||||
<li>deilann (not yet pictured)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +219,7 @@
|
||||
{{ $t('commGuidePara014') }}<br>
|
||||
<em>
|
||||
Lemoness, lefnire, Slappybag, litenull, Shaner, Bobbyroberts99, wc8,
|
||||
deilann, Breadstrings, Megan, Blade, and Daniel the Bard
|
||||
Breadstrings, Megan, Blade, and Daniel the Bard
|
||||
</em>
|
||||
</p>
|
||||
<h2 id="final">
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
href="http://blog.habitrpg.com/"
|
||||
href="https://blog.habitrpg.com/"
|
||||
target="_blank"
|
||||
>{{ $t('tumblr') }}</a>
|
||||
</li>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<noscript class="banner">
|
||||
{{ $t('jsDisabledHeadingFull') }}
|
||||
<br />
|
||||
<a href="http://www.enable-javascript.com/" target="_blank">{{ $t('jsDisabledLink') }}</a>
|
||||
<a href="https://www.enable-javascript.com/" target="_blank">{{ $t('jsDisabledLink') }}</a>
|
||||
</noscript>
|
||||
<div
|
||||
id="intro-signup"
|
||||
|
||||
@@ -137,15 +137,15 @@
|
||||
browser which allows you to refuse cookies. Further information about the procedure to follow in order to
|
||||
disable cookies can be found on your Internet browser provider’s website via your help screen. You may
|
||||
wish to refer to <a
|
||||
href="http://www.allaboutcookies.org/manage-cookies/index.html"
|
||||
href="https://www.allaboutcookies.org/manage-cookies/index.html"
|
||||
target="_blank"
|
||||
>
|
||||
http://www.allaboutcookies.org/manage-cookies/index.html</a> for information on commonly used browsers.
|
||||
https://www.allaboutcookies.org/manage-cookies/index.html</a> for information on commonly used browsers.
|
||||
For more information about targeting and advertising cookies and how you can opt out, you can also visit
|
||||
<a
|
||||
href="http://optout.aboutads.info"
|
||||
href="https://optout.aboutads.info"
|
||||
target="_blank"
|
||||
>http://optout.aboutads.info</a>. Please be aware
|
||||
>https://optout.aboutads.info</a>. Please be aware
|
||||
that if cookies are disabled, not all features of the Service may operate properly or as intended.
|
||||
</p>
|
||||
<h3>Third-Party Analytics Providers</h3>
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
<p>WITHIN THIRTY (30) DAYS OF YOUR PREMIUM PAYMENT DATE AS SHOWN ON YOUR PAYMENT BILL, YOU CAN REQUEST A FULL REFUND BY CONTACTING US AT ADMIN@HABITICA.COM. AFTER THIRTY (30) DAYS OF YOUR PREMIUM PAYMENT DATE, ANY PAYMENT REFUND IS SOLELY SUBJECT TO OUR DISCRETION. THE REFUND SHALL BE YOUR SOLE AND EXCLUSIVE REMEDY.</p>
|
||||
<p>
|
||||
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a
|
||||
href="http://reportaproblem.apple.com"
|
||||
href="https://reportaproblem.apple.com"
|
||||
target="_blank"
|
||||
>http://reportaproblem.apple.com</a>. APPLE'S APP STORE DOES NOT ALLOW DEVELOPERS TO ISSUE REFUND FOR APP STORE PURCHASES MADE BY CUSTOMERS.
|
||||
>https://reportaproblem.apple.com</a>. APPLE'S APP STORE DOES NOT ALLOW DEVELOPERS TO ISSUE REFUND FOR APP STORE PURCHASES MADE BY CUSTOMERS.
|
||||
</p>
|
||||
<h2>Warranty Disclaimer</h2>
|
||||
<p>THE SERVICE AND ANY CONTENT MADE AVAILABLE BY HABITRPG VIA THE SERVICE IS PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT ANY WARRANTIES OF ANY KIND, INCLUDING, WITHOUT LIMITATION, THAT THE SERVICE OR CONTENT WILL OPERATE ERROR-FREE OR THAT THE SERVICE OR CONTENT OR ITS SERVERS ARE FREE OF COMPUTER VIRUSES OR SIMILAR CONTAMINATION OR DESTRUCTIVE FEATURES.</p>
|
||||
|
||||
@@ -268,17 +268,33 @@
|
||||
<span v-if="task.type === 'daily'">{{ task.streak }}</span>
|
||||
<span v-if="task.type === 'habit'">
|
||||
<span
|
||||
v-if="task.up"
|
||||
v-if="task.up && task.counterUp != 0 && task.down"
|
||||
class="m-0"
|
||||
>+{{ task.counterUp }}</span>
|
||||
<span
|
||||
v-else-if=" task.counterUp !=0 && task.counterDown ==0"
|
||||
class="m-0"
|
||||
>{{ task.counterUp }}</span>
|
||||
<span
|
||||
v-else-if="task.up"
|
||||
class="m-0"
|
||||
>0</span>
|
||||
<span
|
||||
v-if="task.up && task.down"
|
||||
class="m-0"
|
||||
> | </span>
|
||||
<span
|
||||
v-if="task.down"
|
||||
v-if="task.down && task.counterDown != 0 && task.up"
|
||||
class="m-0"
|
||||
>-{{ task.counterDown }}</span>
|
||||
<span
|
||||
v-else-if="task.counterDown !=0 && task.counterUp ==0"
|
||||
class="m-0"
|
||||
>{{ task.counterDown }}</span>
|
||||
<span
|
||||
v-else-if="task.down"
|
||||
class="m-0"
|
||||
>0</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -662,6 +662,11 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.custom-control-input {
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
@@ -1280,12 +1285,17 @@ export default {
|
||||
createTag: 'tags:createTag',
|
||||
}),
|
||||
async syncTask () {
|
||||
if (this.task && this.task.group && this.task.group.managerNotes) {
|
||||
if (this.task?.group?.managerNotes) {
|
||||
this.managerNotes = this.task.group.managerNotes;
|
||||
}
|
||||
if (this.groupId && this.task.group && this.task.group.approval) {
|
||||
if (this.groupId && this.task.group?.approval) {
|
||||
this.requiresApproval = this.task.group.approval.required;
|
||||
}
|
||||
if (this.task?.group?.sharedCompletion) {
|
||||
this.sharedCompletion = this.task.group.sharedCompletion;
|
||||
} else if (this.task.group) {
|
||||
this.sharedCompletion = 'singleCompletion';
|
||||
}
|
||||
|
||||
if (this.groupId) {
|
||||
const members = await this.$store.dispatch('members:getGroupMembers', {
|
||||
@@ -1306,9 +1316,6 @@ export default {
|
||||
if (this.task.group && this.task.group.assignedUsers) {
|
||||
this.assignedMembers = this.task.group.assignedUsers;
|
||||
}
|
||||
if (this.task.group) {
|
||||
this.sharedCompletion = this.task.group.sharedCompletion || 'singleCompletion';
|
||||
}
|
||||
}
|
||||
|
||||
// @TODO: This whole component is mutating a prop
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 offset-md-3 text-center nav">
|
||||
<div class="text-center nav">
|
||||
<div
|
||||
class="nav-item"
|
||||
:class="{active: selectedPage === 'profile'}"
|
||||
@@ -470,6 +470,7 @@
|
||||
.gift-icon svg {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -538,6 +539,7 @@
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
min-height: 40px;
|
||||
justify-content: center;
|
||||
@@ -710,6 +712,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 990px) {
|
||||
.profile-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
.profile-actions :not(:last-child) {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.profile-actions {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
.member-details {
|
||||
flex-direction: column;
|
||||
}
|
||||
.member-details .avatar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -969,7 +992,8 @@ export default {
|
||||
axios.post(`/api/v4/user/block/${this.user._id}`);
|
||||
},
|
||||
openSendGemsModal () {
|
||||
this.$root.$emit('habitica::send-gems', this.user);
|
||||
this.$store.state.giftModalOptions.startingPage = 'buyGems';
|
||||
this.$root.$emit('habitica::send-gift', this.user);
|
||||
},
|
||||
adminTurnOnShadowMuting () {
|
||||
if (!this.hero.flags) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
class="standard-page"
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="stats-section-equipment col-12 col-md-6">
|
||||
<h2 class="text-center">
|
||||
{{ $t('equipment') }}
|
||||
</h2>
|
||||
@@ -12,7 +12,7 @@
|
||||
<div
|
||||
v-for="(label, key) in equipTypes"
|
||||
:key="key"
|
||||
class="col-12 col-md-4 item-wrapper"
|
||||
class="item-wrapper"
|
||||
>
|
||||
<div
|
||||
v-if="label !== 'skip'"
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="stats-section-costume col-12 col-md-6">
|
||||
<h2 class="text-center">
|
||||
{{ $t('costume') }}
|
||||
</h2>
|
||||
@@ -57,7 +57,7 @@
|
||||
<div
|
||||
v-for="(label, key) in equipTypes"
|
||||
:key="key"
|
||||
class="col-12 col-md-4 item-wrapper"
|
||||
class="item-wrapper"
|
||||
>
|
||||
<!-- Append a "C" to the key name since HTML IDs have to be unique.-->
|
||||
<div
|
||||
@@ -111,7 +111,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row pet-mount-row">
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="stats-section-pets col-12 col-md-6">
|
||||
<h2
|
||||
v-once
|
||||
class="text-center"
|
||||
@@ -119,8 +119,7 @@
|
||||
{{ $t('pets') }}
|
||||
</h2>
|
||||
<div class="well pet-mount-well">
|
||||
<div class="row col-12">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="pet-mount-well-image">
|
||||
<div
|
||||
class="box"
|
||||
:class="{white: user.items.currentPet}"
|
||||
@@ -131,7 +130,7 @@
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="pet-mount-well-text">
|
||||
<div>{{ formatAnimal(user.items.currentPet, 'pet') }}</div>
|
||||
<div>
|
||||
<strong>{{ $t('petsFound') }}:</strong>
|
||||
@@ -142,10 +141,9 @@
|
||||
{{ beastMasterProgress(user.items.pets) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="stats-section-mounts col-12 col-md-6">
|
||||
<h2
|
||||
v-once
|
||||
class="text-center"
|
||||
@@ -153,28 +151,26 @@
|
||||
{{ $t('mounts') }}
|
||||
</h2>
|
||||
<div class="well pet-mount-well">
|
||||
<div class="row col-12">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="pet-mount-well-image">
|
||||
<div
|
||||
class="box"
|
||||
:class="{white: user.items.currentMount}"
|
||||
>
|
||||
<div
|
||||
class="box"
|
||||
:class="{white: user.items.currentMount}"
|
||||
>
|
||||
<div
|
||||
class="mount"
|
||||
:class="`Mount_Icon_${user.items.currentMount}`"
|
||||
></div>
|
||||
</div>
|
||||
class="mount"
|
||||
:class="`Mount_Icon_${user.items.currentMount}`"
|
||||
></div>
|
||||
</div>
|
||||
<div class="col-12 col-md-8">
|
||||
<div>{{ formatAnimal(user.items.currentMount, 'mount') }}</div>
|
||||
<div>
|
||||
<strong>{{ $t('mountsTamed') }}:</strong>
|
||||
<span>{{ totalCount(user.items.mounts) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $t('mountMasterProgress') }}:</strong>
|
||||
<span>{{ mountMasterProgress(user.items.mounts) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pet-mount-well-text">
|
||||
<div>{{ formatAnimal(user.items.currentMount, 'mount') }}</div>
|
||||
<div>
|
||||
<strong>{{ $t('mountsTamed') }}:</strong>
|
||||
<span>{{ totalCount(user.items.mounts) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $t('mountMasterProgress') }}:</strong>
|
||||
<span>{{ mountMasterProgress(user.items.mounts) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -309,15 +305,13 @@
|
||||
v-if="showStatsSave"
|
||||
class="row save-row"
|
||||
>
|
||||
<div class="col-12 col-md-6 offset-md-3 text-center">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="loading"
|
||||
@click="saveAttributes()"
|
||||
>
|
||||
{{ loading ? $t('loading') : $t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="loading"
|
||||
@click="saveAttributes()"
|
||||
>
|
||||
{{ loading ? $t('loading') : $t('save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -650,10 +644,17 @@ export default {
|
||||
border-radius: 2px;
|
||||
padding: 0.4em;
|
||||
padding-top: 1em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.well.pet-mount-well {
|
||||
padding-left: 15px;
|
||||
padding-bottom: 1em;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
strong {
|
||||
margin-right: .2em;
|
||||
@@ -690,12 +691,13 @@ export default {
|
||||
}
|
||||
|
||||
.save-row {
|
||||
margin-top: 1em;
|
||||
margin: 2em 0 1em 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gear.box {
|
||||
vertical-align: top;
|
||||
margin: 0 auto;
|
||||
// margin: 0 auto;
|
||||
}
|
||||
|
||||
.gear-label {
|
||||
@@ -721,4 +723,34 @@ export default {
|
||||
// breaks the long words without a space
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 850px) {
|
||||
#stats .col-md-6 {
|
||||
flex: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
@media(max-width: 990px) {
|
||||
.modal-body #stats .col-md-6 {
|
||||
flex: none;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
[class^="stats-section-"] {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
#allocation {
|
||||
.box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.col-9 {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.col-9 div:first-child {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -39,6 +39,11 @@ export default [
|
||||
type: 'Moderator',
|
||||
uuid: '28771972-ca6d-4c03-8261-e1734aa7d21d',
|
||||
},
|
||||
{
|
||||
name: 'deilann',
|
||||
type: 'Moderator',
|
||||
uuid: 'e7b5d1e2-3b6e-4192-b867-8bafdb03eeec',
|
||||
},
|
||||
{
|
||||
name: 'Dewines',
|
||||
type: 'Moderator',
|
||||
|
||||
@@ -124,6 +124,9 @@ export default function () {
|
||||
profileOptions: {
|
||||
startingPage: '',
|
||||
},
|
||||
giftModalOptions: {
|
||||
startingPage: '',
|
||||
},
|
||||
rageModalOptions: {
|
||||
npc: '',
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
HabitRPG's translations are managed at http://translate.habitica.com/
|
||||
HabitRPG's translations are managed at https://translate.habitica.com/
|
||||
|
||||
The files in this folder are automatically pulled from there, with
|
||||
exception of the original American-English strings which are managed
|
||||
@@ -10,5 +10,5 @@ request that your changes be translated; changes are automatically
|
||||
copied to the translation website on a regular basis.
|
||||
|
||||
If you want to help with translations, please first read [Guidance for
|
||||
Linguists](http://habitica.fandom.com/wiki/Guidance_for_Linguists) and
|
||||
Linguists](https://habitica.fandom.com/wiki/Guidance_for_Linguists) and
|
||||
note especially its information about the [Translations Trello card](https://trello.com/c/SvTsLdRF/12-translations).
|
||||
|
||||
@@ -178,10 +178,10 @@
|
||||
"advocacy_causes": "Advocacy + Causes",
|
||||
"entertainment": "Entertainment",
|
||||
"finance": "Finance",
|
||||
"health_fitness": "Health + Fitness",
|
||||
"hobbies_occupations": "Hobbies + Occupations",
|
||||
"health_fitness": "الصحة واللياقة البدنية",
|
||||
"hobbies_occupations": "الهوايات والمهن",
|
||||
"location_based": "Location-based",
|
||||
"mental_health": "Mental Health + Self-Care",
|
||||
"mental_health": "الصحة العقلية + العناية الذاتية",
|
||||
"getting_organized": "Getting Organized",
|
||||
"self_improvement": "Self-Improvement",
|
||||
"spirituality": "Spirituality",
|
||||
@@ -192,7 +192,7 @@
|
||||
"emptyMessagesLine1": "You don't have any messages",
|
||||
"emptyMessagesLine2": "Send a message to start a conversation!",
|
||||
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> sent you a message",
|
||||
"letsgo": "Let's Go!",
|
||||
"letsgo": "لنذهب!",
|
||||
"selected": "Selected",
|
||||
"howManyToBuy": "How many would you like to buy?",
|
||||
"contactForm": "Contact the Moderation Team"
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"unlockedReward": "You have received <%= reward %>",
|
||||
"earnedRewardForDevotion": "You have earned <%= reward %> for being committed to improving your life.",
|
||||
"nextRewardUnlocksIn": "Check-ins until your next prize: <%= numberOfCheckinsLeft %>",
|
||||
"awesome": "Awesome!",
|
||||
"countLeft": "Check-ins until next reward: <%= count %>",
|
||||
"incentivesDescription": "When it comes to building habits, consistency is key. Each day you check-in you get closer to a prize.",
|
||||
"checkinEarned": "Your Check-In Counter went up!",
|
||||
"unlockedCheckInReward": "You unlocked a Check-In Prize!",
|
||||
"checkinProgressTitle": "Progress until next",
|
||||
"incentiveBackgroundsUnlockedWithCheckins": "Locked Plain Backgrounds will unlock with Daily Check-Ins.",
|
||||
"oneOfAllPetEggs": "one of each standard Pet Egg",
|
||||
"twoOfAllPetEggs": "two of each standard Pet Egg",
|
||||
"threeOfAllPetEggs": "three of each standard Pet Egg",
|
||||
"oneOfAllHatchingPotions": "one of each standard Hatching Potion",
|
||||
"threeOfEachFood": "three of each standard Pet Food",
|
||||
"fourOfEachFood": "four of each standard Pet Food",
|
||||
"twoSaddles": "two Saddles",
|
||||
"threeSaddles": "three Saddles",
|
||||
"incentiveAchievement": "the Royally Loyal achievement",
|
||||
"royallyLoyal": "Royally Loyal",
|
||||
"royallyLoyalText": "This user has checked in over 500 times, and has earned every Check-In Prize!",
|
||||
"checkInRewards": "Check-In Rewards",
|
||||
"backloggedCheckInRewards": "You received Check-In Prizes! Visit your Inventory and Equipment to see what's new."
|
||||
"unlockedReward": "You have received <%= reward %>",
|
||||
"earnedRewardForDevotion": "You have earned <%= reward %> for being committed to improving your life.",
|
||||
"nextRewardUnlocksIn": "Check-ins until your next prize: <%= numberOfCheckinsLeft %>",
|
||||
"awesome": "Awesome!",
|
||||
"countLeft": "Check-ins until next reward: <%= count %>",
|
||||
"incentivesDescription": "When it comes to building habits, consistency is key. Each day you check-in you get closer to a prize.",
|
||||
"checkinEarned": "Your Check-In Counter went up!",
|
||||
"unlockedCheckInReward": "You unlocked a Check-In Prize!",
|
||||
"checkinProgressTitle": "Progress until next",
|
||||
"incentiveBackgroundsUnlockedWithCheckins": "Locked Plain Backgrounds will unlock with Daily Check-Ins.",
|
||||
"oneOfAllPetEggs": "one of each standard Pet Egg",
|
||||
"twoOfAllPetEggs": "two of each standard Pet Egg",
|
||||
"threeOfAllPetEggs": "three of each standard Pet Egg",
|
||||
"oneOfAllHatchingPotions": "one of each standard Hatching Potion",
|
||||
"threeOfEachFood": "three of each standard Pet Food",
|
||||
"fourOfEachFood": "four of each standard Pet Food",
|
||||
"twoSaddles": "two Saddles",
|
||||
"threeSaddles": "ثلاثة سروج",
|
||||
"incentiveAchievement": "the Royally Loyal achievement",
|
||||
"royallyLoyal": "Royally Loyal",
|
||||
"royallyLoyalText": "This user has checked in over 500 times, and has earned every Check-In Prize!",
|
||||
"checkInRewards": "Check-In Rewards",
|
||||
"backloggedCheckInRewards": "You received Check-In Prizes! Visit your Inventory and Equipment to see what's new."
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"npc": "شخصية غير قابلة للعب",
|
||||
"npcAchievementName": "<%= key %> NPC",
|
||||
"npcAchievementText": "Backed the Kickstarter project at the maximum level!",
|
||||
"welcomeTo": "Welcome to",
|
||||
"welcomeBack": "Welcome back!",
|
||||
"npcAchievementName": "<%= key %> شخصية غير قابلة للعب",
|
||||
"npcAchievementText": "لقد دعمت مشروع Kickstarter بأقصى مستوى!",
|
||||
"welcomeTo": "مرحبًا بكِ في",
|
||||
"welcomeBack": "مرحباً بعودتك!",
|
||||
"justin": "جستن",
|
||||
"justinIntroMessage1": "Hello there! You must be new here. My name is <strong>Justin</strong>, and I'll be your guide in Habitica.",
|
||||
"justinIntroMessage3": "Great! Now, what are you interested in working on throughout this journey?",
|
||||
"justinIntroMessage1": "أهلاً بك! يبدو أنك جديد/ة هنا. اسمي <strong>جاستن</strong> ، وسأكون دليلك في Habitica.",
|
||||
"justinIntroMessage3": "رائعة! الآن ، ما الذي تهتم بالعمل عليه طوال هذه المغامرة؟",
|
||||
"justinIntroMessageUsername": "Before we begin, let’s figure out what to call you. Below you’ll find a display name and username I’ve generated for you. After you’ve picked a display name and username, we’ll get started by creating an avatar!",
|
||||
"justinIntroMessageAppearance": "So how would you like to look? Don’t worry, you can change this later.",
|
||||
"introTour": "Here we are! I've filled out some Tasks for you based on your interests, so you can get started right away. Click a Task to edit or add new Tasks to fit your routine!",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"pets": "الحيوانات الأليفة",
|
||||
"stable": "مستقر",
|
||||
"stable": "الإسطبل",
|
||||
"noActivePet": "لا يوجد حيوان أليف نشط",
|
||||
"activePet": "أنشطة الحيوانات الأليفة",
|
||||
"activePet": "الحيوان الأليف النشط",
|
||||
"raisedPet": "لقد نمت <٪ = حيوان أليف٪>!",
|
||||
"feedPet": "تغذية <٪ = text٪> إلى <٪ = name٪>؟",
|
||||
"mountNotOwned": "أنت لاتملك هذه الكمية.",
|
||||
@@ -32,7 +32,7 @@
|
||||
"etherealLion": "الأسد السماوي",
|
||||
"magicMounts": "جرعة سحرية متصاعدة",
|
||||
"questMounts": "تنقيب جبال",
|
||||
"mountsTamed": "ترويض الجبل",
|
||||
"mountsTamed": "المراكيب المروضة",
|
||||
"noSaddlesAvailable": "أنت لاتملك أي سروج.",
|
||||
"noFoodAvailable": "أنت لاتملك أي أغذية للحيوانات الأليفة.",
|
||||
"food": "أغذية الحيوانات الأليفة والسروج",
|
||||
@@ -61,20 +61,21 @@
|
||||
"veteranFox": "الثعلب المخضرم",
|
||||
"veteranBear": "الدب المخضرم",
|
||||
"veteranLion": "الأسد المخضرم",
|
||||
"activeMount": "جبل نشط",
|
||||
"mounts": "يتصاعد",
|
||||
"activeMount": "مركوب نشط",
|
||||
"mounts": "مركوب",
|
||||
"wackyPets": "حيوانات أليف مضحكة",
|
||||
"magicPets": "دواء سحري للحيوانات الأليفة",
|
||||
"petsFound": "إنشاء حيوانات أليفة",
|
||||
"keyToPets": "مفتاح بيوت الحيوانات",
|
||||
"noActiveMount": "لا يوجد تثبيت نشط",
|
||||
"noActiveMount": "لا يوجد مركوب نشط",
|
||||
"questPets": "بحث الحيوانات",
|
||||
"releasePetsConfirm": "هل أنت متأكد أنك تريد إطلاق سراح حيوانك الأليف القياسي؟",
|
||||
"keyToMounts": "مفتاح بيت الحيوان",
|
||||
"petsReleased": "أفرج عن الحيوانات الأليفة",
|
||||
"keyToPetsDesc": "حرر جميع الحيوانات المسموح بها حتى تتمكن من جمعها مرة أخرى. (لا تتأثر بالحيوانات الأليفة والحيوانات الأليفة الغريبة.)",
|
||||
"petName": "<%= potion(locale) %> <%= egg(locale) %>",
|
||||
"keyToPetsDesc": "حرر جميع الحيوانات الأليفة القياسية حتى تتمكن من جمعها مرة أخرى. (لا تتأثر بالحيوانات الأليفة من المهام والحيوانات الأليفة الغريبة.)",
|
||||
"petName": "<%= egg(locale) %> <%= potion(locale) %>",
|
||||
"keyToMountsDesc": "حرر جميع العينات القياسية حتى تتمكن من جمعها مرة أخرى. (لا تتأثر عمليات تثبيت المهام وعمليات التثبيت النادرة.)",
|
||||
"keyToBoth": "مفاتيح رئيسية لبيوت الكلاب",
|
||||
"releasePetsSuccess": "تم إطلاق حيوانك الأليف القياسي!"
|
||||
"releasePetsSuccess": "تم إطلاق حيوانك الأليف القياسي!",
|
||||
"mountName": "<%= mount(locale) %> <%= potion(locale) %>"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user