Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
848883736d | ||
|
|
28149202db | ||
|
|
4b23cd9f23 | ||
|
|
8aaabdc086 | ||
|
|
6e6ca05352 | ||
|
|
8d1ebff7e9 | ||
|
|
993df72708 | ||
|
|
50d3226a86 | ||
|
|
f964e3c0a5 | ||
|
|
f25fe9e263 | ||
|
|
6eb06fb054 | ||
|
|
286c8c7530 | ||
|
|
47ab8f2073 | ||
|
|
83353f6481 | ||
|
|
9cbd7ad62d | ||
|
|
3485a1d0bc | ||
|
|
2e5106fda1 | ||
|
|
2e5f5714e4 | ||
|
|
3cf7b2c96c | ||
|
|
286db39478 | ||
|
|
4d4c1cfaf3 | ||
|
|
d7ad3efabf | ||
|
|
f8876fe055 | ||
|
|
b973335d69 | ||
|
|
3b6fce0708 | ||
|
|
ff6bd6de71 | ||
|
|
042afe1df3 | ||
|
|
a208ba4aba | ||
|
|
0e958fd306 | ||
|
|
d98fe79e9c | ||
|
|
0e5a811b98 | ||
|
|
a28aea65f8 | ||
|
|
0f92349902 | ||
|
|
d4881cb73a | ||
|
|
b3216fdb85 | ||
|
|
3e37941e0a | ||
|
|
32088767ac | ||
|
|
f4d021ab8c | ||
|
|
8532203717 | ||
|
|
365daba6fc | ||
|
|
70692752c7 | ||
|
|
95d4016678 | ||
|
|
061457b268 | ||
|
|
e7ec9a6d65 | ||
|
|
d1396e7bc6 | ||
|
|
d5cedaa925 | ||
|
|
bea813b318 | ||
|
|
b31268fbc2 | ||
|
|
35727228f0 | ||
|
|
feb7ab8345 | ||
|
|
bba2e71af3 | ||
|
|
26123ac6ae | ||
|
|
addee73e4d | ||
|
|
9736ef0d25 | ||
|
|
638259b885 | ||
|
|
d2f0d7b20b | ||
|
|
3c9f7ff9d8 |
@@ -16,5 +16,3 @@ migrations/*
|
||||
scripts/*
|
||||
website/common/browserify.js
|
||||
Gruntfile.js
|
||||
gulpfile.js
|
||||
gulp
|
||||
2
.github/CONTRIBUTING.md
vendored
@@ -4,7 +4,7 @@
|
||||
|
||||
# Pull Request
|
||||
|
||||
[Please see these instructions for adding a pull request](http://habitica.wikia.com/wiki/Using_Habitica_Git#Pull_Request)
|
||||
[Please see these instructions for adding a pull request](http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API)
|
||||
|
||||
# Requesting a feature
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,7 +6,7 @@
|
||||
|
||||
[//]: # (For more guidelines see https://github.com/HabitRPG/habitica/issues/2760)
|
||||
|
||||
[//]: # (Fill out relevant information - UUID is found in Settings -> API)
|
||||
[//]: # (Fill out relevant information - UUID is found from the Habitia website at User Icon > Settings > API)
|
||||
### General Info
|
||||
* UUID:
|
||||
* Browser:
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,4 +1,4 @@
|
||||
[//]: # (Note: See http://habitica.wikia.com/wiki/Using_Habitica_Git#Pull_Request for more info)
|
||||
[//]: # (Note: See http://habitica.wikia.com/wiki/Using_Your_Local_Install_to_Modify_Habitica%27s_Website_and_API for more info)
|
||||
|
||||
[//]: # (Put Issue # or URL here, if applicable. This will automatically close the issue if your PR is merged in)
|
||||
Fixes put_issue_url_here
|
||||
@@ -8,7 +8,7 @@ Fixes put_issue_url_here
|
||||
|
||||
|
||||
|
||||
[//]: # (Put User ID in here - found in Settings -> API)
|
||||
[//]: # (Put User ID in here - found on the Habitica website at User Icon > Settings > API)
|
||||
|
||||
----
|
||||
UUID:
|
||||
|
||||
@@ -34,5 +34,4 @@ env:
|
||||
- TEST="test:sanity"
|
||||
- TEST="test:content" COVERAGE=true
|
||||
- TEST="test:common" COVERAGE=true
|
||||
- TEST="client:unit" COVERAGE=true
|
||||
- TEST="apidoc"
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
FROM node:boron
|
||||
|
||||
ENV ADMIN_EMAIL admin@habitica.com
|
||||
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
|
||||
ENV AMAZON_PAYMENTS_SELLER_ID AMQ3SB4SG5E91
|
||||
ENV AMPLITUDE_KEY e8d4c24b3d6ef3ee73eeba715023dd43
|
||||
ENV BASE_URL https://habitica.com
|
||||
ENV FACEBOOK_KEY 128307497299777
|
||||
ENV GA_ID UA-33510635-1
|
||||
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
|
||||
ENV NODE_ENV production
|
||||
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA
|
||||
|
||||
# Upgrade NPM to v5 (Yarn is needed because of this bug https://github.com/npm/npm/issues/16807)
|
||||
# The used solution is suggested here https://github.com/npm/npm/issues/16807#issuecomment-313591975
|
||||
RUN yarn global add npm@5
|
||||
@@ -9,7 +20,7 @@ RUN npm install -g gulp mocha
|
||||
# Clone Habitica repo and install dependencies
|
||||
RUN mkdir -p /usr/src/habitrpg
|
||||
WORKDIR /usr/src/habitrpg
|
||||
RUN git clone --branch v4.0.3 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN git clone --branch v4.10.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
|
||||
RUN npm install
|
||||
RUN gulp build:prod --force
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true,
|
||||
},
|
||||
"extends": [
|
||||
"habitrpg/server",
|
||||
"habitrpg/babel"
|
||||
],
|
||||
}
|
||||
@@ -22,5 +22,5 @@ gulp.task('apidoc', ['apidoc:clean'], (done) => {
|
||||
});
|
||||
|
||||
gulp.task('apidoc:watch', ['apidoc'], () => {
|
||||
return gulp.watch(APIDOC_SRC_PATH + '/**/*.js', ['apidoc']);
|
||||
return gulp.watch(`${APIDOC_SRC_PATH}/**/*.js`, ['apidoc']);
|
||||
});
|
||||
|
||||
@@ -8,10 +8,10 @@ const BOOSTRAP_NEW_CONFIG_PATH = 'website/client/assets/scss/bootstrap_config.sc
|
||||
const BOOTSTRAP_ORIGINAL_CONFIG_PATH = 'node_modules/bootstrap/scss/_custom.scss';
|
||||
|
||||
// https://stackoverflow.com/a/14387791/969528
|
||||
function copyFile(source, target, cb) {
|
||||
function copyFile (source, target, cb) {
|
||||
let cbCalled = false;
|
||||
|
||||
function done(err) {
|
||||
function done (err) {
|
||||
if (!cbCalled) {
|
||||
cb(err);
|
||||
cbCalled = true;
|
||||
@@ -33,4 +33,4 @@ gulp.task('bootstrap', (done) => {
|
||||
BOOTSTRAP_ORIGINAL_CONFIG_PATH,
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import gulp from 'gulp';
|
||||
import runSequence from 'run-sequence';
|
||||
import babel from 'gulp-babel';
|
||||
import webpackProductionBuild from '../webpack/build';
|
||||
|
||||
gulp.task('build', () => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
gulp.start('build:prod');
|
||||
}
|
||||
});
|
||||
@@ -27,12 +26,12 @@ gulp.task('build:server', ['build:src', 'build:common']);
|
||||
gulp.task('build:client', ['bootstrap'], (done) => {
|
||||
webpackProductionBuild((err, output) => {
|
||||
if (err) return done(err);
|
||||
console.log(output);
|
||||
console.log(output); // eslint-disable-line no-console
|
||||
});
|
||||
});
|
||||
|
||||
gulp.task('build:prod', [
|
||||
'build:server',
|
||||
'build:server',
|
||||
'build:client',
|
||||
'apidoc',
|
||||
]);
|
||||
|
||||
@@ -7,10 +7,11 @@ import gulp from 'gulp';
|
||||
|
||||
// Add additional properties to the repl's context
|
||||
let improveRepl = (context) => {
|
||||
|
||||
// Let "exit" and "quit" terminate the console
|
||||
['exit', 'quit'].forEach((term) => {
|
||||
Object.defineProperty(context, term, { get () { process.exit(); }});
|
||||
Object.defineProperty(context, term, { get () {
|
||||
process.exit();
|
||||
}});
|
||||
});
|
||||
|
||||
// "clear" clears the screen
|
||||
@@ -18,12 +19,12 @@ let improveRepl = (context) => {
|
||||
process.stdout.write('\u001B[2J\u001B[0;0f');
|
||||
}});
|
||||
|
||||
context.Challenge = require('../website/server/models/challenge').model;
|
||||
context.Group = require('../website/server/models/group').model;
|
||||
context.User = require('../website/server/models/user').model;
|
||||
context.Challenge = require('../website/server/models/challenge').model; // eslint-disable-line global-require
|
||||
context.Group = require('../website/server/models/group').model; // eslint-disable-line global-require
|
||||
context.User = require('../website/server/models/user').model; // eslint-disable-line global-require
|
||||
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
var mongooseOptions = !isProd ? {} : {
|
||||
const isProd = nconf.get('NODE_ENV') === 'production';
|
||||
const mongooseOptions = !isProd ? {} : {
|
||||
replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
||||
server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
||||
};
|
||||
@@ -31,16 +32,15 @@ let improveRepl = (context) => {
|
||||
mongoose.connect(
|
||||
nconf.get('NODE_DB_URI'),
|
||||
mongooseOptions,
|
||||
function (err) {
|
||||
(err) => {
|
||||
if (err) throw err;
|
||||
logger.info('Connected with Mongoose');
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
gulp.task('console', (cb) => {
|
||||
gulp.task('console', () => {
|
||||
improveRepl(repl.start({
|
||||
prompt: 'Habitica > ',
|
||||
}).context);
|
||||
|
||||
@@ -14,75 +14,35 @@ const MAX_SPRITESHEET_SIZE = 1024 * 1024 * 3;
|
||||
const IMG_DIST_PATH = 'website/static/sprites/';
|
||||
const CSS_DIST_PATH = 'website/client/assets/css/sprites/';
|
||||
|
||||
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
|
||||
function checkForSpecialTreatment (name) {
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/;
|
||||
return name.match(regex) || name === 'head_0';
|
||||
}
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
let mainSrc = sync('website/raw_sprites/spritesmith/**/*.png');
|
||||
return createSpritesStream('main', mainSrc);
|
||||
});
|
||||
function calculateImgDimensions (img, addPadding) {
|
||||
let dims = sizeOf(img);
|
||||
|
||||
gulp.task('sprites:largeSprites', () => {
|
||||
let largeSrc = sync('website/raw_sprites/spritesmith_large/**/*.png');
|
||||
return createSpritesStream('largeSprites', largeSrc);
|
||||
});
|
||||
|
||||
gulp.task('sprites:clean', (done) => {
|
||||
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
|
||||
});
|
||||
|
||||
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
|
||||
console.log('Verifiying that images do not exceed max dimensions');
|
||||
|
||||
let numberOfSheetsThatAreTooBig = 0;
|
||||
|
||||
let distSpritesheets = sync(`${IMG_DIST_PATH}*.png`);
|
||||
|
||||
each(distSpritesheets, (img, index) => {
|
||||
let spriteSize = calculateImgDimensions(img);
|
||||
|
||||
if (spriteSize > MAX_SPRITESHEET_SIZE) {
|
||||
numberOfSheetsThatAreTooBig++;
|
||||
let name = basename(img, '.png');
|
||||
console.error(`WARNING: ${name} might be too big - ${spriteSize} > ${MAX_SPRITESHEET_SIZE}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (numberOfSheetsThatAreTooBig > 0) {
|
||||
console.error(`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle them, but there is a margin of error in these calculations so it is probably okay. Mention this to an admin so they can test a staging site on mobile Safari after your PR is merged.`); // https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
|
||||
} else {
|
||||
console.log('All images are within the correct dimensions');
|
||||
let requiresSpecialTreatment = checkForSpecialTreatment(img);
|
||||
if (requiresSpecialTreatment) {
|
||||
let newWidth = dims.width < 90 ? 90 : dims.width;
|
||||
let newHeight = dims.height < 90 ? 90 : dims.height;
|
||||
dims = {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function createSpritesStream (name, src) {
|
||||
let spritesheetSliceIndicies = calculateSpritesheetsSrcIndicies(src);
|
||||
let stream = mergeStream();
|
||||
let padding = 0;
|
||||
|
||||
each(spritesheetSliceIndicies, (start, index) => {
|
||||
let slicedSrc = src.slice(start, spritesheetSliceIndicies[index + 1]);
|
||||
if (addPadding) {
|
||||
padding = dims.width * 8 + dims.height * 8;
|
||||
}
|
||||
|
||||
let spriteData = gulp.src(slicedSrc)
|
||||
.pipe(spritesmith({
|
||||
imgName: `spritesmith-${name}-${index}.png`,
|
||||
cssName: `spritesmith-${name}-${index}.css`,
|
||||
algorithm: 'binary-tree',
|
||||
padding: 1,
|
||||
cssTemplate: 'website/raw_sprites/css/css.template.handlebars',
|
||||
cssVarMap: cssVarMap,
|
||||
}));
|
||||
if (!dims.width || !dims.height) console.error('MISSING DIMENSIONS:', dims); // eslint-disable-line no-console
|
||||
|
||||
let imgStream = spriteData.img
|
||||
.pipe(imagemin())
|
||||
.pipe(gulp.dest(IMG_DIST_PATH));
|
||||
let totalPixelSize = dims.width * dims.height + padding;
|
||||
|
||||
let cssStream = spriteData.css
|
||||
.pipe(gulp.dest(CSS_DIST_PATH));
|
||||
|
||||
stream.add(imgStream);
|
||||
stream.add(cssStream);
|
||||
});
|
||||
|
||||
return stream;
|
||||
return totalPixelSize;
|
||||
}
|
||||
|
||||
function calculateSpritesheetsSrcIndicies (src) {
|
||||
@@ -102,37 +62,6 @@ function calculateSpritesheetsSrcIndicies (src) {
|
||||
return slices;
|
||||
}
|
||||
|
||||
function calculateImgDimensions (img, addPadding) {
|
||||
let dims = sizeOf(img);
|
||||
|
||||
let requiresSpecialTreatment = checkForSpecialTreatment(img);
|
||||
if (requiresSpecialTreatment) {
|
||||
let newWidth = dims.width < 90 ? 90 : dims.width;
|
||||
let newHeight = dims.height < 90 ? 90 : dims.height;
|
||||
dims = {
|
||||
width: newWidth,
|
||||
height: newHeight,
|
||||
};
|
||||
}
|
||||
|
||||
let padding = 0;
|
||||
|
||||
if (addPadding) {
|
||||
padding = (dims.width * 8) + (dims.height * 8);
|
||||
}
|
||||
|
||||
if (!dims.width || !dims.height) console.error('MISSING DIMENSIONS:', dims);
|
||||
|
||||
let totalPixelSize = (dims.width * dims.height) + padding;
|
||||
|
||||
return totalPixelSize;
|
||||
}
|
||||
|
||||
function checkForSpecialTreatment (name) {
|
||||
let regex = /^hair|skin|beard|mustach|shirt|flower|^headAccessory_special_\w+Ears|^eyewear_special_\w+TopFrame/;
|
||||
return name.match(regex) || name === 'head_0';
|
||||
}
|
||||
|
||||
function cssVarMap (sprite) {
|
||||
// For hair, skins, beards, etc. we want to output a '.customize-options.WHATEVER' class, which works as a
|
||||
// 60x60 image pointing at the proper part of the 90x90 sprite.
|
||||
@@ -141,18 +70,93 @@ function cssVarMap (sprite) {
|
||||
if (requiresSpecialTreatment) {
|
||||
sprite.custom = {
|
||||
px: {
|
||||
offset_x: `-${ sprite.x + 25 }px`,
|
||||
offset_y: `-${ sprite.y + 15 }px`,
|
||||
offsetX: `-${ sprite.x + 25 }px`,
|
||||
offsetY: `-${ sprite.y + 15 }px`,
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (~sprite.name.indexOf('shirt'))
|
||||
sprite.custom.px.offset_y = `-${ sprite.y + 30 }px`; // even more for shirts
|
||||
if (~sprite.name.indexOf('hair_base')) {
|
||||
let styleArray = sprite.name.split('_').slice(2,3);
|
||||
if (sprite.name.indexOf('shirt') !== -1)
|
||||
sprite.custom.px.offsetY = `-${ sprite.y + 35 }px`; // even more for shirts
|
||||
if (sprite.name.indexOf('hair_base') !== -1) {
|
||||
let styleArray = sprite.name.split('_').slice(2, 3);
|
||||
if (Number(styleArray[0]) > 14)
|
||||
sprite.custom.px.offset_y = `-${ sprite.y }px`; // don't crop updos
|
||||
sprite.custom.px.offsetY = `-${ sprite.y }px`; // don't crop updos
|
||||
}
|
||||
}
|
||||
|
||||
function createSpritesStream (name, src) {
|
||||
let spritesheetSliceIndicies = calculateSpritesheetsSrcIndicies(src);
|
||||
let stream = mergeStream();
|
||||
|
||||
each(spritesheetSliceIndicies, (start, index) => {
|
||||
let slicedSrc = src.slice(start, spritesheetSliceIndicies[index + 1]);
|
||||
|
||||
let spriteData = gulp.src(slicedSrc)
|
||||
.pipe(spritesmith({
|
||||
imgName: `spritesmith-${name}-${index}.png`,
|
||||
cssName: `spritesmith-${name}-${index}.css`,
|
||||
algorithm: 'binary-tree',
|
||||
padding: 1,
|
||||
cssTemplate: 'website/raw_sprites/css/css.template.handlebars',
|
||||
cssVarMap,
|
||||
}));
|
||||
|
||||
let imgStream = spriteData.img
|
||||
.pipe(imagemin())
|
||||
.pipe(gulp.dest(IMG_DIST_PATH));
|
||||
|
||||
let cssStream = spriteData.css
|
||||
.pipe(gulp.dest(CSS_DIST_PATH));
|
||||
|
||||
stream.add(imgStream);
|
||||
stream.add(cssStream);
|
||||
});
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
gulp.task('sprites:compile', ['sprites:clean', 'sprites:main', 'sprites:largeSprites', 'sprites:checkCompiledDimensions']);
|
||||
|
||||
gulp.task('sprites:main', () => {
|
||||
let mainSrc = sync('website/raw_sprites/spritesmith/**/*.png');
|
||||
return createSpritesStream('main', mainSrc);
|
||||
});
|
||||
|
||||
gulp.task('sprites:largeSprites', () => {
|
||||
let largeSrc = sync('website/raw_sprites/spritesmith_large/**/*.png');
|
||||
return createSpritesStream('largeSprites', largeSrc);
|
||||
});
|
||||
|
||||
gulp.task('sprites:clean', (done) => {
|
||||
clean(`${IMG_DIST_PATH}spritesmith*,${CSS_DIST_PATH}spritesmith*}`, done);
|
||||
});
|
||||
|
||||
gulp.task('sprites:checkCompiledDimensions', ['sprites:main', 'sprites:largeSprites'], () => {
|
||||
console.log('Verifiying that images do not exceed max dimensions'); // eslint-disable-line no-console
|
||||
|
||||
let numberOfSheetsThatAreTooBig = 0;
|
||||
|
||||
let distSpritesheets = sync(`${IMG_DIST_PATH}*.png`);
|
||||
|
||||
each(distSpritesheets, (img) => {
|
||||
let spriteSize = calculateImgDimensions(img);
|
||||
|
||||
if (spriteSize > MAX_SPRITESHEET_SIZE) {
|
||||
numberOfSheetsThatAreTooBig++;
|
||||
let name = basename(img, '.png');
|
||||
console.error(`WARNING: ${name} might be too big - ${spriteSize} > ${MAX_SPRITESHEET_SIZE}`); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
if (numberOfSheetsThatAreTooBig > 0) {
|
||||
// https://github.com/HabitRPG/habitica/pull/6683#issuecomment-185462180
|
||||
console.error( // eslint-disable-line no-console
|
||||
`${numberOfSheetsThatAreTooBig} sheets might too big for mobile Safari to be able to handle
|
||||
them, but there is a margin of error in these calculations so it is probably okay. Mention
|
||||
this to an admin so they can test a staging site on mobile Safari after your PR is merged.`);
|
||||
} else {
|
||||
console.log('All images are within the correct dimensions'); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
import {
|
||||
pipe,
|
||||
awaitPort,
|
||||
kill,
|
||||
runMochaTests,
|
||||
} from './taskHelper';
|
||||
import { server as karma } from 'karma';
|
||||
import mongoose from 'mongoose';
|
||||
import { exec } from 'child_process';
|
||||
import psTree from 'ps-tree';
|
||||
import gulp from 'gulp';
|
||||
import Bluebird from 'bluebird';
|
||||
import runSequence from 'run-sequence';
|
||||
import os from 'os';
|
||||
import nconf from 'nconf';
|
||||
import fs from 'fs';
|
||||
|
||||
const i18n = require('../website/server/libs/i18n');
|
||||
|
||||
// TODO rewrite
|
||||
|
||||
@@ -24,7 +15,6 @@ let server;
|
||||
|
||||
const TEST_DB_URI = nconf.get('TEST_DB_URI');
|
||||
|
||||
const API_V3_TEST_COMMAND = 'npm run test:api-v3';
|
||||
const SANITY_TEST_COMMAND = 'npm run test:sanity';
|
||||
const COMMON_TEST_COMMAND = 'npm run test:common';
|
||||
const CONTENT_TEST_COMMAND = 'npm run test:content';
|
||||
@@ -34,14 +24,14 @@ const CONTENT_OPTIONS = {maxBuffer: 1024 * 500};
|
||||
let testResults = [];
|
||||
let testCount = (stdout, regexp) => {
|
||||
let match = stdout.match(regexp);
|
||||
return parseInt(match && match[1] || 0);
|
||||
return parseInt(match && match[1] || 0, 10);
|
||||
};
|
||||
|
||||
let testBin = (string, additionalEnvVariables = '') => {
|
||||
if (os.platform() === 'win32') {
|
||||
if (additionalEnvVariables != '') {
|
||||
if (additionalEnvVariables !== '') {
|
||||
additionalEnvVariables = additionalEnvVariables.split(' ').join('&&set ');
|
||||
additionalEnvVariables = 'set ' + additionalEnvVariables + '&&';
|
||||
additionalEnvVariables = `set ${additionalEnvVariables}&&`;
|
||||
}
|
||||
return `set NODE_ENV=test&&${additionalEnvVariables}${string}`;
|
||||
} else {
|
||||
@@ -49,9 +39,9 @@ let testBin = (string, additionalEnvVariables = '') => {
|
||||
}
|
||||
};
|
||||
|
||||
gulp.task('test:nodemon', (done) => {
|
||||
process.env.PORT = TEST_SERVER_PORT;
|
||||
process.env.NODE_DB_URI = TEST_DB_URI;
|
||||
gulp.task('test:nodemon', () => {
|
||||
process.env.PORT = TEST_SERVER_PORT; // eslint-disable-line no-process-env
|
||||
process.env.NODE_DB_URI = TEST_DB_URI; // eslint-disable-line no-process-env
|
||||
|
||||
runSequence('nodemon');
|
||||
});
|
||||
@@ -68,8 +58,12 @@ gulp.task('test:prepare:mongo', (cb) => {
|
||||
gulp.task('test:prepare:server', ['test:prepare:mongo'], () => {
|
||||
if (!server) {
|
||||
server = exec(testBin('node ./website/server/index.js', `NODE_DB_URI=${TEST_DB_URI} PORT=${TEST_SERVER_PORT}`), (error, stdout, stderr) => {
|
||||
if (error) { throw `Problem with the server: ${error}`; }
|
||||
if (stderr) { console.error(stderr); }
|
||||
if (error) {
|
||||
throw new Error(`Problem with the server: ${error}`);
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(stderr); // eslint-disable-line no-console
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -84,7 +78,7 @@ gulp.task('test:prepare', [
|
||||
gulp.task('test:sanity', (cb) => {
|
||||
let runner = exec(
|
||||
testBin(SANITY_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -97,7 +91,7 @@ gulp.task('test:sanity', (cb) => {
|
||||
gulp.task('test:common', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(COMMON_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -118,7 +112,7 @@ gulp.task('test:common:watch', ['test:common:clean'], () => {
|
||||
gulp.task('test:common:safe', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(COMMON_TEST_COMMAND),
|
||||
(err, stdout, stderr) => {
|
||||
(err, stdout) => { // eslint-disable-line handle-callback-err
|
||||
testResults.push({
|
||||
suite: 'Common Specs\t',
|
||||
pass: testCount(stdout, /(\d+) passing/),
|
||||
@@ -135,7 +129,7 @@ gulp.task('test:content', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -157,7 +151,7 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
let runner = exec(
|
||||
testBin(CONTENT_TEST_COMMAND),
|
||||
CONTENT_OPTIONS,
|
||||
(err, stdout, stderr) => {
|
||||
(err, stdout) => { // eslint-disable-line handle-callback-err
|
||||
testResults.push({
|
||||
suite: 'Content Specs\t',
|
||||
pass: testCount(stdout, /(\d+) passing/),
|
||||
@@ -173,7 +167,7 @@ gulp.task('test:content:safe', ['test:prepare:build'], (cb) => {
|
||||
gulp.task('test:api-v3:unit', (done) => {
|
||||
let runner = exec(
|
||||
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-unit --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/unit --recursive --require ./test/helpers/start-server'),
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -192,7 +186,7 @@ gulp.task('test:api-v3:integration', (done) => {
|
||||
let runner = exec(
|
||||
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => {
|
||||
(err) => {
|
||||
if (err) {
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -212,7 +206,7 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => done(err)
|
||||
(err) => done(err)
|
||||
);
|
||||
|
||||
pipe(runner);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import fs from 'fs';
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
import gulp from 'gulp';
|
||||
import { postToSlack, conf } from './taskHelper';
|
||||
|
||||
@@ -12,8 +11,82 @@ const SLACK_CONFIG = {
|
||||
|
||||
const LOCALES = './website/common/locales/';
|
||||
const ENGLISH_LOCALE = `${LOCALES}en/`;
|
||||
|
||||
|
||||
function getArrayOfLanguages () {
|
||||
let languages = fs.readdirSync(LOCALES);
|
||||
languages.shift(); // Remove README.md from array of languages
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
const ALL_LANGUAGES = getArrayOfLanguages();
|
||||
|
||||
function stripOutNonJsonFiles (collection) {
|
||||
let onlyJson = _.filter(collection, (file) => {
|
||||
return file.match(/[a-zA-Z]*\.json/);
|
||||
});
|
||||
|
||||
return onlyJson;
|
||||
}
|
||||
|
||||
function eachTranslationFile (languages, cb) {
|
||||
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
|
||||
|
||||
_.each(languages, (lang) => {
|
||||
_.each(jsonFiles, (filename) => {
|
||||
let parsedTranslationFile;
|
||||
try {
|
||||
const translationFile = fs.readFileSync(`${LOCALES}${lang}/${filename}`);
|
||||
parsedTranslationFile = JSON.parse(translationFile);
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
let englishFile = fs.readFileSync(ENGLISH_LOCALE + filename);
|
||||
let parsedEnglishFile = JSON.parse(englishFile);
|
||||
|
||||
cb(null, lang, filename, parsedEnglishFile, parsedTranslationFile);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function eachTranslationString (languages, cb) {
|
||||
eachTranslationFile(languages, (error, language, filename, englishJSON, translationJSON) => {
|
||||
if (error) return;
|
||||
_.each(englishJSON, (string, key) => {
|
||||
const translationString = translationJSON[key];
|
||||
cb(language, filename, key, string, translationString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatMessageForPosting (msg, items) {
|
||||
let body = `*Warning:* ${msg}`;
|
||||
body += '\n\n```\n';
|
||||
body += items.join('\n');
|
||||
body += '\n```';
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function getStringsWith (json, interpolationRegex) {
|
||||
let strings = {};
|
||||
|
||||
_.each(json, (fileName) => {
|
||||
const rawFile = fs.readFileSync(ENGLISH_LOCALE + fileName);
|
||||
const parsedJson = JSON.parse(rawFile);
|
||||
|
||||
strings[fileName] = {};
|
||||
_.each(parsedJson, (value, key) => {
|
||||
const match = value.match(interpolationRegex);
|
||||
if (match) strings[fileName][key] = match;
|
||||
});
|
||||
});
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
const malformedStringExceptions = {
|
||||
messageDropFood: true,
|
||||
armoireFood: true,
|
||||
@@ -23,7 +96,6 @@ const malformedStringExceptions = {
|
||||
gulp.task('transifex', ['transifex:missingFiles', 'transifex:missingStrings', 'transifex:malformedStrings']);
|
||||
|
||||
gulp.task('transifex:missingFiles', () => {
|
||||
|
||||
let missingStrings = [];
|
||||
|
||||
eachTranslationFile(ALL_LANGUAGES, (error) => {
|
||||
@@ -40,7 +112,6 @@ gulp.task('transifex:missingFiles', () => {
|
||||
});
|
||||
|
||||
gulp.task('transifex:missingStrings', () => {
|
||||
|
||||
let missingStrings = [];
|
||||
|
||||
eachTranslationString(ALL_LANGUAGES, (language, filename, key, englishString, translationString) => {
|
||||
@@ -58,7 +129,6 @@ gulp.task('transifex:missingStrings', () => {
|
||||
});
|
||||
|
||||
gulp.task('transifex:malformedStrings', () => {
|
||||
|
||||
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
|
||||
let interpolationRegex = /<%= [a-zA-Z]* %>/g;
|
||||
let stringsToLookFor = getStringsWith(jsonFiles, interpolationRegex);
|
||||
@@ -66,25 +136,23 @@ gulp.task('transifex:malformedStrings', () => {
|
||||
let stringsWithMalformedInterpolations = [];
|
||||
let stringsWithIncorrectNumberOfInterpolations = [];
|
||||
|
||||
let count = 0;
|
||||
_.each(ALL_LANGUAGES, function (lang) {
|
||||
|
||||
_.each(stringsToLookFor, function (strings, file) {
|
||||
let translationFile = fs.readFileSync(LOCALES + lang + '/' + file);
|
||||
_.each(ALL_LANGUAGES, (lang) => {
|
||||
_.each(stringsToLookFor, (strings, filename) => {
|
||||
let translationFile = fs.readFileSync(`${LOCALES}${lang}/${filename}`);
|
||||
let parsedTranslationFile = JSON.parse(translationFile);
|
||||
|
||||
_.each(strings, function (value, key) {
|
||||
_.each(strings, (value, key) => { // eslint-disable-line max-nested-callbacks
|
||||
let translationString = parsedTranslationFile[key];
|
||||
if (!translationString) return;
|
||||
|
||||
let englishOccurences = stringsToLookFor[file][key];
|
||||
let englishOccurences = stringsToLookFor[filename][key];
|
||||
let translationOccurences = translationString.match(interpolationRegex);
|
||||
|
||||
if (!translationOccurences) {
|
||||
let malformedString = `${lang} - ${file} - ${key} - ${translationString}`;
|
||||
let malformedString = `${lang} - ${filename} - ${key} - ${translationString}`;
|
||||
stringsWithMalformedInterpolations.push(malformedString);
|
||||
} else if (englishOccurences.length !== translationOccurences.length && !malformedStringExceptions[key]) {
|
||||
let missingInterpolationString = `${lang} - ${file} - ${key} - ${translationString}`;
|
||||
let missingInterpolationString = `${lang} - ${filename} - ${key} - ${translationString}`;
|
||||
stringsWithIncorrectNumberOfInterpolations.push(missingInterpolationString);
|
||||
}
|
||||
});
|
||||
@@ -103,74 +171,3 @@ gulp.task('transifex:malformedStrings', () => {
|
||||
postToSlack(formattedMessage, SLACK_CONFIG);
|
||||
}
|
||||
});
|
||||
|
||||
function getArrayOfLanguages () {
|
||||
let languages = fs.readdirSync(LOCALES);
|
||||
languages.shift(); // Remove README.md from array of languages
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
function eachTranslationFile (languages, cb) {
|
||||
let jsonFiles = stripOutNonJsonFiles(fs.readdirSync(ENGLISH_LOCALE));
|
||||
|
||||
_.each(languages, (lang) => {
|
||||
_.each(jsonFiles, (filename) => {
|
||||
try {
|
||||
var translationFile = fs.readFileSync(LOCALES + lang + '/' + filename);
|
||||
var parsedTranslationFile = JSON.parse(translationFile);
|
||||
} catch (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
let englishFile = fs.readFileSync(ENGLISH_LOCALE + filename);
|
||||
let parsedEnglishFile = JSON.parse(englishFile);
|
||||
|
||||
cb(null, lang, filename, parsedEnglishFile, parsedTranslationFile);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function eachTranslationString (languages, cb) {
|
||||
eachTranslationFile(languages, (error, language, filename, englishJSON, translationJSON) => {
|
||||
if (error) return;
|
||||
_.each(englishJSON, (string, key) => {
|
||||
var translationString = translationJSON[key];
|
||||
cb(language, filename, key, string, translationString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatMessageForPosting (msg, items) {
|
||||
let body = `*Warning:* ${msg}`;
|
||||
body += '\n\n```\n';
|
||||
body += items.join('\n');
|
||||
body += '\n```';
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function getStringsWith (json, interpolationRegex) {
|
||||
var strings = {};
|
||||
|
||||
_.each(json, function (file_name) {
|
||||
var raw_file = fs.readFileSync(ENGLISH_LOCALE + file_name);
|
||||
var parsed_json = JSON.parse(raw_file);
|
||||
|
||||
strings[file_name] = {};
|
||||
_.each(parsed_json, function (value, key) {
|
||||
var match = value.match(interpolationRegex);
|
||||
if (match) strings[file_name][key] = match;
|
||||
});
|
||||
});
|
||||
|
||||
return strings;
|
||||
}
|
||||
|
||||
function stripOutNonJsonFiles (collection) {
|
||||
let onlyJson = _.filter(collection, (file) => {
|
||||
return file.match(/[a-zA-Z]*\.json/);
|
||||
});
|
||||
|
||||
return onlyJson;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { resolve } from 'path';
|
||||
* Get access to configruable values
|
||||
*/
|
||||
nconf.argv().env().file({ file: 'config.json' });
|
||||
export var conf = nconf;
|
||||
export const conf = nconf;
|
||||
|
||||
/*
|
||||
* Kill a child process and any sub-children that process may have spawned.
|
||||
@@ -26,11 +26,12 @@ export function kill (proc) {
|
||||
pids.forEach(kill); return;
|
||||
}
|
||||
try {
|
||||
exec(/^win/.test(process.platform)
|
||||
? `taskkill /PID ${pid} /T /F`
|
||||
: `kill -9 ${pid}`);
|
||||
exec(/^win/.test(process.platform) ?
|
||||
`taskkill /PID ${pid} /T /F` :
|
||||
`kill -9 ${pid}`);
|
||||
} catch (e) {
|
||||
console.log(e); // eslint-disable-line no-console
|
||||
}
|
||||
catch (e) { console.log(e); }
|
||||
});
|
||||
};
|
||||
|
||||
@@ -44,21 +45,25 @@ export function kill (proc) {
|
||||
* before failing.
|
||||
*/
|
||||
export function awaitPort (port, max = 60) {
|
||||
return new Bluebird((reject, resolve) => {
|
||||
let socket, timeout, interval;
|
||||
return new Bluebird((rej, res) => {
|
||||
let socket;
|
||||
let timeout;
|
||||
let interval;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
reject(`Timed out after ${max} seconds`);
|
||||
rej(`Timed out after ${max} seconds`);
|
||||
}, max * 1000);
|
||||
|
||||
interval = setInterval(() => {
|
||||
socket = net.connect({port: port}, () => {
|
||||
socket = net.connect({port}, () => {
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
socket.destroy();
|
||||
resolve();
|
||||
}).on('error', () => { socket.destroy; });
|
||||
res();
|
||||
}).on('error', () => {
|
||||
socket.destroy();
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
@@ -67,8 +72,12 @@ export function awaitPort (port, max = 60) {
|
||||
* Pipe the child's stdin and stderr to the parent process.
|
||||
*/
|
||||
export function pipe (child) {
|
||||
child.stdout.on('data', (data) => { process.stdout.write(data); });
|
||||
child.stderr.on('data', (data) => { process.stderr.write(data); });
|
||||
child.stdout.on('data', (data) => {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
child.stderr.on('data', (data) => {
|
||||
process.stderr.write(data);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -78,8 +87,8 @@ export function postToSlack (msg, config = {}) {
|
||||
let slackUrl = nconf.get('SLACK_URL');
|
||||
|
||||
if (!slackUrl) {
|
||||
console.error('No slack post url specified. Your message was:');
|
||||
console.log(msg);
|
||||
console.error('No slack post url specified. Your message was:'); // eslint-disable-line no-console
|
||||
console.log(msg); // eslint-disable-line no-console
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -89,15 +98,15 @@ export function postToSlack (msg, config = {}) {
|
||||
channel: `#${config.channel || '#general'}`,
|
||||
username: config.username || 'gulp task',
|
||||
text: msg,
|
||||
icon_emoji: `:${config.emoji || 'gulp'}:`,
|
||||
icon_emoji: `:${config.emoji || 'gulp'}:`, // eslint-disable-line camelcase
|
||||
})
|
||||
.end((err, res) => {
|
||||
if (err) console.error('Unable to post to slack', err);
|
||||
.end((err) => {
|
||||
if (err) console.error('Unable to post to slack', err); // eslint-disable-line no-console
|
||||
});
|
||||
}
|
||||
|
||||
export function runMochaTests (files, server, cb) {
|
||||
require('../test/helpers/globals.helper');
|
||||
require('../test/helpers/globals.helper'); // eslint-disable-line global-require
|
||||
|
||||
let mocha = new Mocha({reporter: 'spec'});
|
||||
let tests = glob(files);
|
||||
@@ -108,7 +117,7 @@ export function runMochaTests (files, server, cb) {
|
||||
});
|
||||
|
||||
mocha.run((numberOfFailures) => {
|
||||
if (!process.env.RUN_INTEGRATION_TEST_FOREVER) {
|
||||
if (!process.env.RUN_INTEGRATION_TEST_FOREVER) { // eslint-disable-line no-process-env
|
||||
if (server) kill(server);
|
||||
process.exit(numberOfFailures);
|
||||
}
|
||||
|
||||
12
gulpfile.js
@@ -8,11 +8,11 @@
|
||||
|
||||
require('babel-register');
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
require('./gulp/gulp-apidoc');
|
||||
require('./gulp/gulp-build');
|
||||
require('./gulp/gulp-bootstrap');
|
||||
if (process.env.NODE_ENV === 'production') { // eslint-disable-line no-process-env
|
||||
require('./gulp/gulp-apidoc'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-build'); // eslint-disable-line global-require
|
||||
require('./gulp/gulp-bootstrap'); // eslint-disable-line global-require
|
||||
} else {
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require);
|
||||
require('gulp').task('default', ['test']);
|
||||
require('glob').sync('./gulp/gulp-*').forEach(require); // eslint-disable-line global-require
|
||||
require('gulp').task('default', ['test']); // eslint-disable-line global-require
|
||||
}
|
||||
|
||||
111
migrations/20171030_jackolanterns.js
Normal file
@@ -0,0 +1,111 @@
|
||||
var migrationName = '20171030_jackolanterns.js';
|
||||
var authorName = 'Sabe'; // in case script author needs to know when their ...
|
||||
var authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; //... own data is done
|
||||
|
||||
/*
|
||||
* Award the Jack-O'-Lantern ladder:
|
||||
* Ghost Jack-O-Lantern Mount to owners of Ghost Jack-O-Lantern Pet
|
||||
* Ghost Jack-O-Lantern Pet to owners of Jack-O-Lantern Mount
|
||||
* Jack-O-Lantern Mount to owners of Jack-O-Lantern Pet
|
||||
* Jack-O-Lantern Pet to everyone else
|
||||
*/
|
||||
|
||||
var monk = require('monk');
|
||||
var connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
|
||||
var dbUsers = monk(connectionString).get('users', { castIds: false });
|
||||
|
||||
function processUsers(lastId) {
|
||||
// specify a query to limit the affected users (empty for all users):
|
||||
var query = {
|
||||
'migration':{$ne:migrationName},
|
||||
};
|
||||
|
||||
if (lastId) {
|
||||
query._id = {
|
||||
$gt: lastId
|
||||
}
|
||||
}
|
||||
|
||||
dbUsers.find(query, {
|
||||
sort: {_id: 1},
|
||||
limit: 250,
|
||||
fields: [
|
||||
'items.pets',
|
||||
'items.mounts',
|
||||
] // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
})
|
||||
.then(updateUsers)
|
||||
.catch(function (err) {
|
||||
console.log(err);
|
||||
return exiting(1, 'ERROR! ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
var progressCount = 1000;
|
||||
var count = 0;
|
||||
|
||||
function updateUsers (users) {
|
||||
if (!users || users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
displayData();
|
||||
return;
|
||||
}
|
||||
|
||||
var userPromises = users.map(updateUser);
|
||||
var lastUser = users[users.length - 1];
|
||||
|
||||
return Promise.all(userPromises)
|
||||
.then(function () {
|
||||
processUsers(lastUser._id);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUser (user) {
|
||||
count++;
|
||||
|
||||
var set = {};
|
||||
var inc = {
|
||||
'items.food.Candy_Skeleton': 1,
|
||||
'items.food.Candy_Base': 1,
|
||||
'items.food.Candy_CottonCandyBlue': 1,
|
||||
'items.food.Candy_CottonCandyPink': 1,
|
||||
'items.food.Candy_Shade': 1,
|
||||
'items.food.Candy_White': 1,
|
||||
'items.food.Candy_Golden': 1,
|
||||
'items.food.Candy_Zombie': 1,
|
||||
'items.food.Candy_Desert': 1,
|
||||
'items.food.Candy_Red': 1,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Ghost']) {
|
||||
set = {'migration':migrationName, 'items.mounts.JackOLantern-Ghost': true};
|
||||
} else if (user && user.items && user.items.mounts && user.items.mounts['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Ghost': 5};
|
||||
} else if (user && user.items && user.items.pets && user.items.pets['JackOLantern-Base']) {
|
||||
set = {'migration':migrationName, 'items.mounts.JackOLantern-Base': true};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.pets.JackOLantern-Base': 5};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set, $inc:inc});
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
}
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' users processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
module.exports = processUsers;
|
||||
@@ -65,19 +65,29 @@ function updateUser (user) {
|
||||
set = {'migration':migrationName};
|
||||
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.back_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.back_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.body_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.head_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.armor_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_takeThis', '_id': monk.id()}};
|
||||
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
|
||||
set = {'migration':migrationName, 'items.gear.owned.weapon_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.weapon_special_takeThis', '_id': monk.id()}};
|
||||
} else {
|
||||
set = {'migration':migrationName, 'items.gear.owned.shield_special_takeThis':false};
|
||||
var push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.shield_special_takeThis', '_id': monk.id()}};
|
||||
}
|
||||
|
||||
dbUsers.update({_id: user._id}, {$set:set});
|
||||
if (push) {
|
||||
dbUsers.update({_id: user._id}, {$set: set, $push: push});
|
||||
} else {
|
||||
dbUsers.update({_id: user._id}, {$set: set});
|
||||
}
|
||||
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + user._id);
|
||||
if (user._id == authorUuid) console.warn(authorName + ' processed');
|
||||
|
||||
1686
package-lock.json
generated
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.6.2",
|
||||
"version": "4.10.1",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
@@ -116,12 +116,12 @@
|
||||
"validator": "^4.9.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"vue": "^2.1.0",
|
||||
"vue-loader": "^11.0.0",
|
||||
"vue": "^2.5.2",
|
||||
"vue-loader": "^13.3.0",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^2.0.0-rc.5",
|
||||
"vue-router": "^3.0.0",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-template-compiler": "^2.1.10",
|
||||
"vue-template-compiler": "^2.5.2",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#45e607a7bccf4e3e089761b3b7b33e3f2c5dc21f",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-merge": "^4.0.0",
|
||||
@@ -136,7 +136,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "npm run lint && gulp test && npm run client:unit && gulp apidoc",
|
||||
"test": "npm run lint && gulp test && gulp apidoc",
|
||||
"test:build": "gulp test:prepare:build",
|
||||
"test:api-v3": "gulp test:api-v3",
|
||||
"test:api-v3:unit": "gulp test:api-v3:unit",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-v3-integration.helper';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('GET challenges/groups/:groupId', () => {
|
||||
context('Public Guild', () => {
|
||||
@@ -181,4 +182,123 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
expect(foundChallengeIndex).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
context('Party', () => {
|
||||
let party, user, nonMember, challenge, challenge2;
|
||||
|
||||
before(async () => {
|
||||
let { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'TestParty',
|
||||
type: 'party',
|
||||
},
|
||||
});
|
||||
|
||||
party = group;
|
||||
user = groupLeader;
|
||||
|
||||
nonMember = await generateUser();
|
||||
|
||||
challenge = await generateChallenge(user, group);
|
||||
challenge2 = await generateChallenge(user, group);
|
||||
});
|
||||
|
||||
it('should prevent non-member from seeing challenges', async () => {
|
||||
await expect(nonMember.get(`/challenges/groups/${party._id}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return group challenges for member with populated leader', async () => {
|
||||
let challenges = await user.get(`/challenges/groups/${party._id}`);
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return group challenges for member using ID "party"', async () => {
|
||||
let challenges = await user.get('/challenges/groups/party');
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Tavern', () => {
|
||||
let tavern, user, challenge, challenge2;
|
||||
|
||||
before(async () => {
|
||||
user = await generateUser();
|
||||
await user.update({balance: 0.5});
|
||||
tavern = await user.get(`/groups/${TAVERN_ID}`);
|
||||
|
||||
challenge = await generateChallenge(user, tavern, {prize: 1});
|
||||
challenge2 = await generateChallenge(user, tavern, {prize: 1});
|
||||
});
|
||||
|
||||
it('should return tavern challenges with populated leader', async () => {
|
||||
let challenges = await user.get(`/challenges/groups/${TAVERN_ID}`);
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return tavern challenges using ID "habitrpg', async () => {
|
||||
let challenges = await user.get('/challenges/groups/habitrpg');
|
||||
|
||||
let foundChallenge1 = _.find(challenges, { _id: challenge._id });
|
||||
expect(foundChallenge1).to.exist;
|
||||
expect(foundChallenge1.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
expect(foundChallenge2.leader).to.eql({
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate', () => {
|
||||
let user;
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate-bulk', () => {
|
||||
let user;
|
||||
const statsUpdate = {
|
||||
stats: {
|
||||
con: 1,
|
||||
str: 2,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
it('returns an error if user does not have enough points', async () => {
|
||||
await expect(user.post('/user/allocate-bulk', statsUpdate))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notEnoughAttrPoints'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allocates attribute points', async () => {
|
||||
await user.update({'stats.points': 3});
|
||||
|
||||
await user.post('/user/allocate-bulk', statsUpdate);
|
||||
await user.sync();
|
||||
|
||||
expect(user.stats.con).to.equal(1);
|
||||
expect(user.stats.str).to.equal(2);
|
||||
expect(user.stats.points).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate-now', () => {
|
||||
// More tests in common code unit tests
|
||||
@@ -365,6 +365,72 @@ describe('cron', () => {
|
||||
|
||||
expect(user.history.todos).to.be.lengthOf(1);
|
||||
});
|
||||
|
||||
it('should remove completed todos from users taskOrder list', () => {
|
||||
tasksByType.todos = [];
|
||||
user.tasksOrder.todos = [];
|
||||
let todo = {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
user.tasksOrder.todos = tasksByType.todos.map(taskTodo => {
|
||||
return taskTodo._id;
|
||||
});
|
||||
// Since ideally tasksByType should not contain completed todos, fake ids should be filtered too
|
||||
user.tasksOrder.todos.push('00000000-0000-0000-0000-000000000000');
|
||||
|
||||
expect(tasksByType.todos).to.be.lengthOf(2);
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(3);
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
|
||||
expect(tasksByType.todos).to.be.lengthOf(2);
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(1);
|
||||
});
|
||||
|
||||
it('should preserve todos order in task list', () => {
|
||||
tasksByType.todos = [];
|
||||
user.tasksOrder.todos = [];
|
||||
let todo = {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line new-cap
|
||||
tasksByType.todos.push(task);
|
||||
|
||||
// Set up user.tasksOrder list in a specific order
|
||||
user.tasksOrder.todos = tasksByType.todos.map(todoTask => {
|
||||
return todoTask._id;
|
||||
}).reverse();
|
||||
let original = user.tasksOrder.todos; // Preserve the original order
|
||||
|
||||
cron({user, tasksByType, daysMissed, analytics});
|
||||
|
||||
let listsAreEqual = true;
|
||||
user.tasksOrder.todos.forEach((taskId, index) => {
|
||||
if (original[index]._id !== taskId) {
|
||||
listsAreEqual = false;
|
||||
}
|
||||
});
|
||||
|
||||
expect(listsAreEqual);
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(original.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dailys', () => {
|
||||
|
||||
@@ -475,7 +475,7 @@ describe('Purchasing a group plan for group', () => {
|
||||
|
||||
let updatedUser = await User.findById(recipient._id).exec();
|
||||
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 4);
|
||||
expect(updatedUser.purchased.plan.extraMonths).to.within(3, 5);
|
||||
});
|
||||
|
||||
it('adds months to members with existing recurring subscription (Paypal)', async () => {
|
||||
|
||||
@@ -13,6 +13,9 @@ import analyticsService from '../../../../../website/server/libs/analyticsServic
|
||||
import * as cronLib from '../../../../../website/server/libs/cron';
|
||||
import { v4 as generateUUID } from 'uuid';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(60 * 60 * 1000).getTime();
|
||||
const CRON_TIMEOUT_UNIT = new Date(60 * 1000).getTime();
|
||||
|
||||
describe('cron middleware', () => {
|
||||
let res, req;
|
||||
let user;
|
||||
@@ -235,7 +238,13 @@ describe('cron middleware', () => {
|
||||
sandbox.spy(cronLib, 'recoverCron');
|
||||
|
||||
sandbox.stub(User, 'update')
|
||||
.withArgs({ _id: user._id, _cronSignature: 'NOT_RUNNING' })
|
||||
.withArgs({
|
||||
_id: user._id,
|
||||
$or: [
|
||||
{_cronSignature: 'NOT_RUNNING'},
|
||||
{_cronSignature: {$lt: sinon.match.number}},
|
||||
],
|
||||
})
|
||||
.returns({
|
||||
exec () {
|
||||
return Promise.resolve(updatedUser);
|
||||
@@ -251,4 +260,48 @@ describe('cron middleware', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature less than an hour ago should error', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let now = new Date();
|
||||
await User.update({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT + CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
let expectedErrMessage = `Impossible to recover from cron for user ${user._id}.`;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, (err) => {
|
||||
if (!err) return reject(new Error('Cron should have failed.'));
|
||||
expect(err.message).to.be.equal(expectedErrMessage);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('cronSignature longer than an hour ago should allow cron', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let now = new Date();
|
||||
await User.update({
|
||||
_id: user._id,
|
||||
}, {
|
||||
$set: {
|
||||
_cronSignature: now.getTime() - CRON_TIMEOUT_WAIT - CRON_TIMEOUT_UNIT,
|
||||
},
|
||||
}).exec();
|
||||
await user.save();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, (err) => {
|
||||
if (err) return reject(err);
|
||||
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||
expect(user._cronSignature).to.be.equal('NOT_RUNNING');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import allocate from '../../../website/common/script/ops/allocate';
|
||||
import allocate from '../../../../website/common/script/ops/stats/allocate';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocate', () => {
|
||||
let user;
|
||||
98
test/common/ops/stats/allocateBulk.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import allocateBulk from '../../../../website/common/script/ops/stats/allocateBulk';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocateBulk', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
it('throws an error if an invalid attribute is supplied', (done) => {
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
invalid: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidAttribute', {attr: 'invalid'}));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the stats are not supplied', (done) => {
|
||||
try {
|
||||
allocateBulk(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('statsObjectRequired'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have attribute points', (done) => {
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughAttrPoints'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have enough attribute points', (done) => {
|
||||
user.stats.points = 1;
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughAttrPoints'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('allocates attribute points', () => {
|
||||
user.stats.points = 3;
|
||||
expect(user.stats.int).to.equal(0);
|
||||
expect(user.stats.str).to.equal(0);
|
||||
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(user.stats.str).to.equal(2);
|
||||
expect(user.stats.int).to.equal(1);
|
||||
expect(user.stats.points).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import allocateNow from '../../../website/common/script/ops/allocateNow';
|
||||
import allocateNow from '../../../../website/common/script/ops/stats/allocateNow';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocateNow', () => {
|
||||
let user;
|
||||
@@ -71,8 +71,8 @@
|
||||
import axios from 'axios';
|
||||
import { loadProgressBar } from 'axios-progress-bar';
|
||||
|
||||
import AppMenu from './components/appMenu';
|
||||
import AppHeader from './components/appHeader';
|
||||
import AppMenu from './components/header/menu';
|
||||
import AppHeader from './components/header/index';
|
||||
import AppFooter from './components/appFooter';
|
||||
import notificationsDisplay from './components/notifications';
|
||||
import snackbars from './components/snackbars/notifications';
|
||||
@@ -214,15 +214,15 @@ export default {
|
||||
}
|
||||
|
||||
// Verify the client is updated
|
||||
const serverAppVersion = response.data.appVersion;
|
||||
let serverAppVersionState = this.$store.state.serverAppVersion;
|
||||
if (isApiCall && !serverAppVersionState) {
|
||||
this.$store.state.serverAppVersion = serverAppVersion;
|
||||
} else if (isApiCall && serverAppVersionState !== serverAppVersion) {
|
||||
if (document.activeElement.tagName !== 'INPUT' || confirm(this.$t('habiticaHasUpdated'))) {
|
||||
location.reload(true);
|
||||
}
|
||||
}
|
||||
// const serverAppVersion = response.data.appVersion;
|
||||
// let serverAppVersionState = this.$store.state.serverAppVersion;
|
||||
// if (isApiCall && !serverAppVersionState) {
|
||||
// this.$store.state.serverAppVersion = serverAppVersion;
|
||||
// } else if (isApiCall && serverAppVersionState !== serverAppVersion) {
|
||||
// if (document.activeElement.tagName !== 'INPUT' || confirm(this.$t('habiticaHasUpdated'))) {
|
||||
// location.reload(true);
|
||||
// }
|
||||
// }
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
.npc_joyful_reaper {
|
||||
.promo_potions_thunderstorm {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px -250px;
|
||||
width: 141px;
|
||||
height: 441px;
|
||||
}
|
||||
.promo_take_this {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: -142px -250px;
|
||||
width: 114px;
|
||||
height: 87px;
|
||||
}
|
||||
.scene_guilds {
|
||||
background-image: url(/static/sprites/spritesmith-largeSprites-0.png);
|
||||
background-position: 0px 0px;
|
||||
width: 282px;
|
||||
height: 147px;
|
||||
width: 498px;
|
||||
height: 249px;
|
||||
}
|
||||
|
||||
@@ -1,51 +1,171 @@
|
||||
.Pet-Wolf-Ghost {
|
||||
.Pet-Unicorn-Zombie {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -82px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Golden {
|
||||
.Pet-Whale-Base {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -328px -100px;
|
||||
background-position: 0px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Holly {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -246px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Peppermint {
|
||||
.Pet-Whale-CottonCandyBlue {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -164px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Red {
|
||||
.Pet-Whale-CottonCandyPink {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: 0px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-RoyalPurple {
|
||||
.Pet-Whale-Desert {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -82px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Shade {
|
||||
.Pet-Whale-Golden {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -164px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Shimmer {
|
||||
.Pet-Whale-Red {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -246px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Shade {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -246px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Skeleton {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: 0px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-White {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -82px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Whale-Zombie {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -164px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Aquatic {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -246px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Base {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -328px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-CottonCandyBlue {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -328px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-CottonCandyPink {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -328px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Cupid {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: 0px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Desert {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -82px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Ember {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -164px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Fairy {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -246px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Floral {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -328px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Ghost {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Golden {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Holly {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Peppermint {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Red {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -492px 0px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-RoyalPurple {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -492px -100px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Shade {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -492px -200px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Shimmer {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -492px -300px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Skeleton {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: 0px 0px;
|
||||
@@ -54,169 +174,169 @@
|
||||
}
|
||||
.Pet-Wolf-Spooky {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: 0px -200px;
|
||||
background-position: -82px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Thunderstorm {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -82px -200px;
|
||||
background-position: -164px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Veteran {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -164px -200px;
|
||||
background-position: -246px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-White {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -246px -200px;
|
||||
background-position: -328px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Zombie {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -328px 0px;
|
||||
background-position: -410px -400px;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet_HatchingPotion_Aquatic {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -328px -200px;
|
||||
background-position: 0px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Base {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: 0px -300px;
|
||||
background-position: -574px -138px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_CottonCandyBlue {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -69px -300px;
|
||||
background-position: -69px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_CottonCandyPink {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -138px -300px;
|
||||
background-position: -138px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Cupid {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -479px -345px;
|
||||
background-position: -207px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Desert {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -276px -300px;
|
||||
background-position: -276px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Ember {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px 0px;
|
||||
background-position: -345px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Fairy {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px -69px;
|
||||
background-position: -414px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Floral {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px -138px;
|
||||
background-position: -483px -500px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Ghost {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px -207px;
|
||||
background-position: -574px 0px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Golden {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -410px -276px;
|
||||
background-position: -574px -69px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Holly {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: 0px -369px;
|
||||
background-position: -492px -400px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Peppermint {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -69px -369px;
|
||||
background-position: -574px -207px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Purple {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -138px -369px;
|
||||
background-position: -574px -276px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Red {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -207px -369px;
|
||||
background-position: -574px -345px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_RoyalPurple {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -276px -369px;
|
||||
background-position: -574px -414px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Shade {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -345px -369px;
|
||||
background-position: -574px -483px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Shimmer {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -479px 0px;
|
||||
background-position: 0px -569px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Skeleton {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -479px -69px;
|
||||
background-position: -69px -569px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Spooky {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -479px -138px;
|
||||
background-position: -138px -569px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Thunderstorm {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -479px -207px;
|
||||
background-position: -207px -569px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_White {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -479px -276px;
|
||||
background-position: -276px -569px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.Pet_HatchingPotion_Zombie {
|
||||
background-image: url(/static/sprites/spritesmith-main-20.png);
|
||||
background-position: -207px -300px;
|
||||
background-position: -345px -569px;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
BIN
website/client/assets/images/npc/habitoween/quest_shop_npc.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
website/client/assets/images/npc/habitoween/tavern_npc.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
@@ -20,3 +20,11 @@
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
}
|
||||
|
||||
.badge-purple {
|
||||
position: absolute;
|
||||
color: $white;
|
||||
background: $purple-400;
|
||||
line-height: 1.2;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
&-daily-todo-content-disabled {
|
||||
background: $gray-600;
|
||||
|
||||
* {
|
||||
.task-title, .task-notes {
|
||||
color: $gray-300 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// this variables are used to determine which shop npc/backgrounds should be loaded
|
||||
// possible values are: normal, fall
|
||||
// possible values are: normal, fall, habitoween
|
||||
// more to be added on future seasons
|
||||
|
||||
$npc_market_flavor: "fall";
|
||||
$npc_quests_flavor: "fall";
|
||||
$npc_seasonal_flavor: "fall";
|
||||
$npc_timetravelers_flavor: "fall";
|
||||
$npc_tavern_flavor: "fall";
|
||||
$npc_market_flavor: 'normal';
|
||||
$npc_quests_flavor: 'normal';
|
||||
$npc_seasonal_flavor: 'normal';
|
||||
$npc_timetravelers_flavor: 'normal';
|
||||
$npc_tavern_flavor: 'normal';
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="23" viewBox="0 0 40 23">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="2.4">
|
||||
<path d="M23.324 10.53h14.621M2.248 10.53h21.946M16.804 15.667s1.501-.742 3.293-.742a7.57 7.57 0 0 1 3.197.742"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.838 10.53v3.878c0 3.879-3.22 7.052-7.154 7.052H8.359c-3.497 0-6.36-2.822-6.36-6.269v-4.968l8.289-7.205c2.02-1.756 4.63-1.113 6.482.958M23.26 10.53v3.878c0 3.879 3.219 7.052 7.154 7.052h1.325c3.497 0 6.359-2.822 6.359-6.269v-4.968L29.81 3.018c-2.02-1.756-4.63-1.113-6.482.958"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 38.5 21.85"><defs><style>.cls-1{}</style></defs><title>extras</title><path class="cls-1" d="M38.84,13.59l-8.29-7.2c-2.39-2.08-5.74-1.64-8.16,1.06A1.2,1.2,0,0,0,24.17,9c1.17-1.3,3.12-2.31,4.8-.85l6.22,5.41H4.81L11,8.19c1.68-1.46,3.63-.45,4.8.85a1.2,1.2,0,1,0,1.79-1.6c-2.42-2.7-5.77-3.14-8.16-1.06l-8.29,7.2a1.2,1.2,0,0,0-.41.91v5a7.52,7.52,0,0,0,7.56,7.47H9.63a8.35,8.35,0,0,0,8.05-6.08A7,7,0,0,1,20,20.4a6.41,6.41,0,0,1,2.26.44,8.35,8.35,0,0,0,8.05,6.09h1.32a7.52,7.52,0,0,0,7.56-7.47v-5A1.2,1.2,0,0,0,38.84,13.59ZM9.63,24.53H8.31a5.12,5.12,0,0,1-5.16-5.07V16H15.59v2.68A5.91,5.91,0,0,1,9.63,24.53ZM18,18.25V16h4v2.24A8.39,8.39,0,0,0,20,18,8.83,8.83,0,0,0,18,18.25Zm13.7,6.28H30.37a5.91,5.91,0,0,1-6-5.85V16H36.85v3.46A5.12,5.12,0,0,1,31.69,24.53Z" transform="translate(-0.75 -5.08)"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 879 B |
@@ -1,6 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<rect width="29.6" height="29.6" x="1.2" y="1.2" stroke="#A5A1AC" stroke-width="2.4" rx="4"/>
|
||||
<path fill="#A5A1AC" d="M11 11l2-1-2-1-1-2-1 2-2 1 2 1 1 2zM23 14l2-1-2-1-1-2-1 2-2 1 2 1 1 2zM15.333 22.333L18 21l-2.667-1.333L14 17l-1.333 2.667L10 21l2.667 1.333L14 25z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><style>.cls-1{fill-rule:evenodd;}</style></defs><title>background</title><path class="cls-1" d="M28,0H4A4,4,0,0,0,0,4V28a4,4,0,0,0,4,4H28a4,4,0,0,0,4-4V4A4,4,0,0,0,28,0Zm1.6,28A1.6,1.6,0,0,1,28,29.6H4A1.6,1.6,0,0,1,2.4,28V4A1.6,1.6,0,0,1,4,2.4H28A1.6,1.6,0,0,1,29.6,4V28ZM10,13L9,11,7,10,9,9l1-2,1,2,2,1-2,1Zm13-1,2,1-2,1-1,2-1-2-2-1,2-1,1-2Zm-7.67,7.67L18,21l-2.67,1.33L14,25l-1.33-2.67L10,21l2.67-1.33L14,17Z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 519 B |
@@ -1,8 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="34" viewBox="0 0 32 34">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path class='path' stroke="#6133B4" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.4" d="M2.124 20.456S2.854 2 15.734 2c12.88 0 13.61 18.456 13.61 18.456"/>
|
||||
<path class='path' stroke="#6133B4" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.4" d="M10.29 32.388s-1.725-7.96-1.725-14.681c0-6.722 2.382-14.682 2.382-14.682M21.064 32.388s1.724-7.96 1.724-14.681c0-6.722-2.382-14.682-2.382-14.682"/>
|
||||
<path class='path' stroke="#6133B4" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.4" d="M8.957 22.65s3.372-.64 6.933-.64c3.563 0 6.54.64 6.54.64M7.003 32.388h3.29M21.144 32.388h3.289"/>
|
||||
<path fill="#6133B4" d="M3.784 21.533a1.893 1.893 0 1 1-3.785 0 1.893 1.893 0 0 1 3.785 0M31.237 21.533a1.893 1.893 0 1 1-3.786 0 1.893 1.893 0 0 1 3.786 0"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30.49 32"><defs><style>.cls-1{fill-rule:evenodd;}</style></defs><title>body</title><path class="cls-1" d="M30.55,18.8C30.19,12.11,26.77,0,16.11,0A11.9,11.9,0,0,0,11,1.11l-0.12.05C4.35,4.32,2,13.19,1.69,18.64A1.85,1.85,0,1,0,4,19.05c0.09-1.31.83-9.79,5.5-14.14A53.73,53.73,0,0,0,7.94,16.5,76.9,76.9,0,0,0,9.37,29.66H7.59a1.17,1.17,0,0,0,0,2.34h3.21L11,32l0.1,0a1.13,1.13,0,0,0,.23-0.1l0.17-.11a1.12,1.12,0,0,0,.37-0.57,1.12,1.12,0,0,0,0-.2,0.33,0.33,0,0,0,0-.26,1.09,1.09,0,0,0,0-.11c0-.05-0.82-3.82-1.3-8.26a39.75,39.75,0,0,1,5.62-.45,33.72,33.72,0,0,1,5.21.43c-0.48,4.44-1.29,8.23-1.3,8.28a1.17,1.17,0,0,0,.9,1.39l0.25,0H24.6a1.17,1.17,0,0,0,0-2.34H22.74A76.9,76.9,0,0,0,24.17,16.5a54,54,0,0,0-1.6-11.72C27.16,8.91,28.05,17,28.2,18.84A1.85,1.85,0,1,0,30.55,18.8Zm-8.72-2.3c0,1.13-.05,2.3-0.14,3.46a35.84,35.84,0,0,0-5.42-.43,41.52,41.52,0,0,0-5.84.46c-0.09-1.17-.14-2.35-0.14-3.48A57.14,57.14,0,0,1,12.41,3a10.15,10.15,0,0,1,7.28,0A57.16,57.16,0,0,1,21.83,16.5Z" transform="translate(-0.76)"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 960 B After Width: | Height: | Size: 1.1 KiB |
5
website/client/assets/svg/bottom.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#686274" stroke-width="2">
|
||||
<path d="M5,0v8 M1,5l4,4l4-4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 213 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16" viewBox="0 0 14 16">
|
||||
<path fill="#A5A1AC" fill-rule="evenodd" d="M3 14h8V4H3v10zM14 4h-1v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4H0V2h4V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4v2zm-6 8h1V6H8v6zm-3 0h1V6H5v6z"/>
|
||||
<path fill-rule="evenodd" d="M3 14h8V4H3v10zM14 4h-1v10a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4H0V2h4V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1h4v2zm-6 8h1V6H8v6zm-3 0h1V6H5v6z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 274 B After Width: | Height: | Size: 259 B |
3
website/client/assets/svg/edit.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M2.723 11.859l1.418 1.419-2.219.788.8-2.207zm2.762.686l-2.03-2.03 7.386-7.385 2.03 2.03-7.386 7.385zm8.704-10.731c.56.56.56 1.468 0 2.03l-.285.284-2.03-2.03.286-.284a1.438 1.438 0 0 1 2.027 0h.002zM11.125.782l-.8.8-8.417 8.415a.731.731 0 0 0-.098.122s-.012.024-.02.036a.713.713 0 0 0-.048.1v.012L.044 15.022a.73.73 0 0 0 .934.935l4.755-1.704a.728.728 0 0 0 .102-.05l.034-.018a.731.731 0 0 0 .122-.097l9.227-9.213A2.896 2.896 0 0 0 11.125.782z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 570 B |
@@ -1,6 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="35" height="35" viewBox="0 0 35 35">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#A5A1AC" d="M22.688 0a7.222 7.222 0 0 0-5.12 2.12L2.12 17.569a7.241 7.241 0 0 0 0 10.24l2.538 2.538L30.346 4.66 27.809 2.12A7.222 7.222 0 0 0 22.688 0m0 2.414c1.289 0 2.502.502 3.413 1.414l.832.83L4.659 26.934l-.831-.831a4.793 4.793 0 0 1-1.415-3.414c0-1.29.502-2.501 1.415-3.414L19.275 3.828a4.793 4.793 0 0 1 3.413-1.414"/>
|
||||
<path stroke="#A5A1AC" stroke-width="2.4" d="M4.385 28.385l5.746 5.747M28.36 4.41l5.746 5.746M16.372 16.398l5.746 5.746M7.382 25.389l5.746 5.746M10.379 22.392l5.746 5.746M13.376 19.395l5.746 5.746M19.37 13.4l5.745 5.747M22.366 10.404l5.746 5.746M25.363 7.407l5.746 5.746"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.24 31.25"><defs><style>.cls-1{}</style></defs><title>hair</title><path class="cls-1" d="M26.14,6.55l4,4,1.7-1.7L27,4l0,0-0.85-.85-1-1a5.49,5.49,0,0,0-7.76,0L2.21,17.38a5.49,5.49,0,0,0,0,7.76l1,1L4,27l0,0,4.86,4.86,1.7-1.7-4-4,0,0L8.5,24.19l4,4,1.7-1.7-4-4,1.8-1.8,4,4L17.7,23l-4-4,1.8-1.8,4,4,1.7-1.7-4-4,1.8-1.8,4,4L24.7,16l-4-4,1.8-1.8,4,4,1.7-1.7-4-4,1.92-1.92ZM4.88,24.42l-1-1a3.09,3.09,0,0,1,0-4.37L19.08,3.9a3.09,3.09,0,0,1,4.37,0l1,1Z" transform="translate(-0.6 -0.6)"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 768 B After Width: | Height: | Size: 573 B |
3
website/client/assets/svg/menu.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="4" height="16" viewBox="0 0 4 16">
|
||||
<path fill-rule="evenodd" d="M2 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 225 B |
@@ -1,6 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="33" viewBox="0 0 37 33">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-linejoin="round" stroke-width="2.4">
|
||||
<path stroke-linecap="round" d="M13.782 21.846s1.794 3.682 0 5.454c-1.793 1.773-5.957 0-8.327.938C3.085 29.175 2 31.65 2 31.65M23.744 21.846s-1.794 3.682 0 5.454c1.793 1.773 5.957 0 8.327.938 2.37.937 3.455 3.412 3.455 3.412"/>
|
||||
<path d="M27.939 12.642c0 6.321-5.755 11.445-9.277 11.445-2.98 0-9.277-5.124-9.277-11.445C9.385 6.32 13.089 2 18.662 2c5.572 0 9.277 4.32 9.277 10.642z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.8 28.4"><defs><style>.cls-1{}</style></defs><title>skin</title><path class="cls-1" d="M31.8,28.52a7.44,7.44,0,0,0-3.69-3.63,8.72,8.72,0,0,0-3.82-.26c-1.21.09-2.57,0.19-3.08-.3-0.72-.71-0.15-2.6.24-3.4a1.13,1.13,0,0,0,0-.18,12,12,0,0,0,3.75-8.41c0-6.2-3.84-10.53-9.33-10.53S6.58,6.13,6.58,12.33a12.19,12.19,0,0,0,3.94,8.46,1.13,1.13,0,0,0,0,.13c0.39,0.8,1,2.7.24,3.41-0.5.5-1.87,0.39-3.08,0.3a8.75,8.75,0,0,0-3.82.26A7.43,7.43,0,0,0,.2,28.52a1.2,1.2,0,1,0,2.2,1,5,5,0,0,1,2.37-2.36A7.3,7.3,0,0,1,7.53,27c1.72,0.13,3.66.28,4.94-1a3.9,3.9,0,0,0,1-3.11,5.72,5.72,0,0,0,2.39.64,6.15,6.15,0,0,0,2.57-.67,3.92,3.92,0,0,0,1,3.14c1.28,1.26,3.22,1.12,4.94,1a7.34,7.34,0,0,1,2.76.1,5,5,0,0,1,2.37,2.36A1.2,1.2,0,1,0,31.8,28.52ZM9,12.33C9,7.39,11.7,4.2,15.91,4.2s6.93,3.19,6.93,8.13-4.62,8.84-6.93,8.84C14,21.17,9,17.25,9,12.33Z" transform="translate(-0.1 -1.8)"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 948 B |
5
website/client/assets/svg/top.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="11" viewBox="0 0 10 11">
|
||||
<g fill="none" fill-rule="evenodd" stroke="#686274" stroke-width="2">
|
||||
<path d="M5 3v8M9 6L5 2 1 6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 212 B |
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="24" viewBox="0 0 22 24">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M15 13h-.4c1.9-1.2 3.3-3.3 3.4-5.8.1-3.8-3.1-7.2-6.9-7.2C7.1 0 4 3.1 4 7c0 2.6 1.3 4.8 3.4 6H7c-3.9 0-7 3.1-7 7v1c0 1.7 1.3 3 3 3h16c1.7 0 3-1.3 3-3v-1c0-3.9-3.1-7-7-7zM6 7c0-2.8 2.2-5 5-5s5 2.2 5 5-2.2 5-5 5-5-2.2-5-5zm13 15H3c-.6 0-1-.4-1-1v-1c0-2.8 2.2-5 5-5h8c2.8 0 5 2.2 5 5v1c0 .6-.4 1-1 1z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 424 B After Width: | Height: | Size: 424 B |
@@ -1,17 +1,24 @@
|
||||
<template lang="pug">
|
||||
b-modal#login-incentives(:title="data.message", size='md', :hide-footer="true")
|
||||
.modal-body
|
||||
.row
|
||||
h3.col-12.text-center(v-if='data.rewardText') {{ $t('unlockedReward', {reward: data.rewardText}) }}
|
||||
.row.reward-row
|
||||
.col-12
|
||||
avatar.avatar(:member='user', :avatarOnly='true', :withBackground='true')
|
||||
.text-center.col-12
|
||||
.reward-wrap
|
||||
.reward-wrap(v-if="!data.rewardText")
|
||||
div(v-if="nextReward.rewardKey.length === 1", :class="nextReward.rewardKey[0]")
|
||||
.reward(v-for="reward in nextReward.rewardKey", v-if="nextReward.rewardKey.length > 1", :class='reward')
|
||||
.reward-wrap(v-if="data.rewardText")
|
||||
div(v-if="data.rewardKey.length === 1", :class="data.rewardKey[0]")
|
||||
.reward(v-for="reward in data.rewardKey", v-if="data.rewardKey.length > 1", :class='reward')
|
||||
.col-12.text-center(v-if="data.nextRewardAt")
|
||||
h4 {{ $t('countLeft', {count: data.nextRewardAt - user.loginIncentives}) }}
|
||||
.row
|
||||
.col-8.offset-2.text-center
|
||||
.col-12.text-center(v-if='data.rewardText')
|
||||
p {{ $t('earnedRewardForDevotion', {reward: data.rewardText}) }}
|
||||
.col-12.text-center
|
||||
p {{ $t('incentivesDescription') }}
|
||||
.col-12.text-center(v-if="data.nextRewardAt")
|
||||
h3 {{ $t('nextRewardUnlocksIn', {numberOfCheckinsLeft: data.nextRewardAt - user.loginIncentives}) }}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
label
|
||||
strong(v-once) {{$t('endDate')}}
|
||||
b-form-input.end-date-input
|
||||
.form-group
|
||||
.form-group(v-if='creating')
|
||||
label
|
||||
strong(v-once) {{$t('prize')}}
|
||||
input(type='number', :min='minPrize', :max='maxPrize', v-model="workingChallenge.prize")
|
||||
|
||||
@@ -150,6 +150,7 @@ export default {
|
||||
this.filters = eventData;
|
||||
},
|
||||
createChallenge () {
|
||||
this.$store.state.challengeOptions.workingChallenge = {};
|
||||
this.$root.$emit('show::modal', 'challenge-modal');
|
||||
},
|
||||
async loadchallanges () {
|
||||
|
||||
@@ -16,23 +16,23 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
button.btn.btn-secondary(v-once) {{$t('randomize')}}
|
||||
#options-nav.container.section.text-center.customize-menu
|
||||
.row
|
||||
div(:class='{"col-3": !editing, "col-2 offset-1": editing}')
|
||||
.menu-container(:class='{"col-3": !editing, "col-2 offset-1": editing, active: activeTopPage === "body"}')
|
||||
.menu-item(@click='changeTopPage("body", "size")')
|
||||
.svg-icon(v-html='icons.bodyIcon')
|
||||
strong(v-once) {{$t('bodyBody')}}
|
||||
div(:class='{"col-3": !editing, "col-2": editing}')
|
||||
.menu-container(:class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "skin"}')
|
||||
.menu-item(@click='changeTopPage("skin", "color")')
|
||||
.svg-icon(v-html='icons.skinIcon')
|
||||
strong(v-once) {{$t('skin')}}
|
||||
div(:class='{"col-3": !editing, "col-2": editing}')
|
||||
.menu-container(:class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "hair"}')
|
||||
.menu-item(@click='changeTopPage("hair", "color")')
|
||||
.svg-icon(v-html='icons.hairIcon')
|
||||
strong(v-once) {{$t('hair')}}
|
||||
div(:class='{"col-3": !editing, "col-2": editing}')
|
||||
.menu-container(:class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "extra"}')
|
||||
.menu-item(@click='changeTopPage("extra", "glasses")')
|
||||
.svg-icon(v-html='icons.accessoriesIcon')
|
||||
strong(v-once) {{$t('extra')}}
|
||||
.col-2(v-if='editing')
|
||||
.menu-container.col-2(v-if='editing', :class='{active: activeTopPage === "backgrounds"}')
|
||||
.menu-item(@click='changeTopPage("backgrounds", "2017")')
|
||||
.svg-icon(v-html='icons.backgroundsIcon')
|
||||
strong(v-once) {{$t('backgrounds')}}
|
||||
@@ -505,14 +505,15 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0 auto;
|
||||
color: #6133B4;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
.menu-container {
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
.menu-container:hover, .menu-container.active {
|
||||
cursor: pointer;
|
||||
svg path, strong {
|
||||
stroke: purple !important;
|
||||
}
|
||||
color: #6133B4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ export default {
|
||||
|
||||
if (this.searchTerm) {
|
||||
sortedMembers = sortedMembers.filter(member => {
|
||||
return member.profile.name.toLowerCase().indexOf(this.searchTerm.toLowerCase) !== -1;
|
||||
return member.profile.name.toLowerCase().indexOf(this.searchTerm.toLowerCase()) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -102,9 +102,9 @@ div
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'client/libs/store';
|
||||
import MemberDetails from './memberDetails';
|
||||
import createPartyModal from './groups/createPartyModal';
|
||||
import membersModal from './groups/membersModal';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import createPartyModal from '../groups/createPartyModal';
|
||||
import membersModal from '../groups/membersModal';
|
||||
import ResizeDirective from 'client/directives/resize.directive';
|
||||
|
||||
export default {
|
||||
@@ -53,42 +53,20 @@ div
|
||||
a.dropdown-item(href="https://trello.com/c/odmhIqyW/440-read-first-table-of-contents", target='_blank') {{ $t('requestAF') }}
|
||||
a.dropdown-item(href="http://habitica.wikia.com/wiki/Contributing_to_Habitica", target='_blank') {{ $t('contributing') }}
|
||||
a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }}
|
||||
.item-with-icon(v-if="userHourglasses > 0")
|
||||
.svg-icon(v-html="icons.hourglasses")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")')
|
||||
span {{userGems | roundBigNumber}}
|
||||
.item-with-icon
|
||||
.svg-icon(v-html="icons.gold")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
a.item-with-icon(@click="sync")
|
||||
.svg-icon(v-html="icons.sync")
|
||||
notification-menu
|
||||
a.dropdown.item-with-icon.item-user
|
||||
span.message-count.top-count(v-if='user.inbox.newMessages > 0') {{user.inbox.newMessages}}
|
||||
.svg-icon.user(v-html="icons.user")
|
||||
.dropdown-menu.dropdown-menu-right.user-dropdown
|
||||
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
||||
h3 {{ user.profile.name }}
|
||||
span.small-text {{ $t('editAvatar') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
|
||||
| {{ $t('messages') }}
|
||||
span.message-count(v-if='user.inbox.newMessages > 0') {{user.inbox.newMessages}}
|
||||
a.dropdown-item(@click='showAvatar("backgrounds", "2017")') {{ $t('backgrounds') }}
|
||||
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
|
||||
a.dropdown-item(@click='showProfile("achievements")') {{ $t('achievements') }}
|
||||
a.dropdown-item.dropdown-separated(@click='showProfile("profile")') {{ $t('profile') }}
|
||||
router-link.dropdown-item(:to="{name: 'site'}") {{ $t('settings') }}
|
||||
router-link.dropdown-item.dropdown-separated(:to="{name: 'subscription'}") {{ $t('subscription') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(to="/", @click.prevent='logout()') {{ $t('logout') }}
|
||||
li(v-if='!this.user.purchased.plan.customerId', @click='showBuyGemsModal("subscribe")')
|
||||
.dropdown-item.text-center
|
||||
h3.purple {{ $t('needMoreGems') }}
|
||||
span.small-text {{ $t('needMoreGemsInfo') }}
|
||||
img.float-left.align-self-end(src='~assets/images/gem-rain.png')
|
||||
button.btn.btn-primary.btn-lg.learn-button Learn More
|
||||
img.float-right.align-self-end(src='~assets/images/gold-rain.png')
|
||||
.d-flex.align-items-center
|
||||
.item-with-icon(v-if="userHourglasses > 0")
|
||||
.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')")
|
||||
span {{ userHourglasses }}
|
||||
.item-with-icon
|
||||
.svg-icon.gem(v-html="icons.gem", @click='showBuyGemsModal("gems")', v-b-tooltip.hover.bottom="$t('gems')")
|
||||
span {{userGems | roundBigNumber}}
|
||||
.item-with-icon.gold
|
||||
.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')")
|
||||
span {{Math.floor(user.stats.gp * 100) / 100}}
|
||||
a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')")
|
||||
.svg-icon(v-html="icons.sync")
|
||||
notification-menu.item-with-icon
|
||||
user-dropdown.item-with-icon
|
||||
b-nav-toggle(target='nav_collapse')
|
||||
</template>
|
||||
|
||||
@@ -165,40 +143,13 @@ div
|
||||
}
|
||||
}
|
||||
|
||||
// Make the dropdown menu open on hover
|
||||
.dropdown:hover .dropdown-menu {
|
||||
display: block;
|
||||
margin-top: 0; // remove the gap so it doesn't close
|
||||
// Make the dropdown menu open on hover
|
||||
.dropdown:hover .dropdown-menu {
|
||||
display: block;
|
||||
margin-top: 0; // remove the gap so it doesn't close
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.dropdown-separated {
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
width: 14.75em;
|
||||
}
|
||||
|
||||
.learn-button {
|
||||
margin: 0.75em 0.75em 0.75em 1em;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
color: $gray-200;
|
||||
font-style: normal;
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.dropdown-menu:not(.user-dropdown) {
|
||||
.dropdown-menu {
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
@@ -230,81 +181,48 @@ div
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.item-with-icon {
|
||||
color: $white;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
padding-top: 16px;
|
||||
padding-left: 16px;
|
||||
white-space: nowrap;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover .svg-icon {
|
||||
&.gold {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
&:hover /deep/ .svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
& /deep/ .svg-icon {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 12px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-notifications, .item-user {
|
||||
padding-right: 12.5px;
|
||||
padding-left: 12.5px;
|
||||
color: $header-color;
|
||||
transition: none;
|
||||
|
||||
.svg-icon {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-user .edit-avatar {
|
||||
h3 {
|
||||
color: $gray-10;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
.menu-icon {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.gem:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
background-color: $red-50;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: .5em;
|
||||
padding: .2em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import bNavToggle from 'bootstrap-vue/lib/components/nav-toggle';
|
||||
import bCollapse from 'bootstrap-vue/lib/components/collapse';
|
||||
|
||||
@@ -313,17 +231,18 @@ import * as Analytics from 'client/libs/analytics';
|
||||
import gemIcon from 'assets/svg/gem.svg';
|
||||
import goldIcon from 'assets/svg/gold.svg';
|
||||
import syncIcon from 'assets/svg/sync.svg';
|
||||
import userIcon from 'assets/svg/user.svg';
|
||||
import svgHourglasses from 'assets/svg/hourglass.svg';
|
||||
import logo from 'assets/svg/logo.svg';
|
||||
import InboxModal from './userMenu/inbox.vue';
|
||||
import notificationMenu from './notificationMenu';
|
||||
import creatorIntro from './creatorIntro';
|
||||
import profile from './userMenu/profile';
|
||||
import markPMSRead from 'common/script/ops/markPMSRead';
|
||||
import InboxModal from '../userMenu/inbox.vue';
|
||||
import notificationMenu from './notificationsDropdown';
|
||||
import creatorIntro from '../creatorIntro';
|
||||
import profile from '../userMenu/profile';
|
||||
import userDropdown from './userDropdown';
|
||||
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
userDropdown,
|
||||
InboxModal,
|
||||
notificationMenu,
|
||||
creatorIntro,
|
||||
@@ -331,12 +250,15 @@ export default {
|
||||
bNavToggle,
|
||||
bCollapse,
|
||||
},
|
||||
directives: {
|
||||
bTooltip,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isUserDropdownOpen: false,
|
||||
icons: Object.freeze({
|
||||
gem: gemIcon,
|
||||
gold: goldIcon,
|
||||
user: userIcon,
|
||||
hourglasses: svgHourglasses,
|
||||
sync: syncIcon,
|
||||
logo,
|
||||
@@ -357,31 +279,15 @@ export default {
|
||||
this.getUserGroupPlans();
|
||||
},
|
||||
methods: {
|
||||
toggleUserDropdown () {
|
||||
this.isUserDropdownOpen = !this.isUserDropdownOpen;
|
||||
},
|
||||
sync () {
|
||||
return Promise.all([
|
||||
this.$store.dispatch('user:fetch', {forceLoad: true}),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
|
||||
]);
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('auth:logout');
|
||||
},
|
||||
showInbox () {
|
||||
markPMSRead(this.user);
|
||||
axios.post('/api/v3/user/mark-pms-read');
|
||||
this.$root.$emit('show::modal', 'inbox-modal');
|
||||
},
|
||||
showAvatar (startingPage, subpage) {
|
||||
this.$store.state.avatarEditorOptions.editingUser = true;
|
||||
this.$store.state.avatarEditorOptions.startingPage = startingPage;
|
||||
this.$store.state.avatarEditorOptions.subpage = subpage;
|
||||
this.$root.$emit('show::modal', 'avatar-modal');
|
||||
},
|
||||
showProfile (startingPage) {
|
||||
this.$store.state.profileUser = this.user;
|
||||
this.$store.state.profileOptions.startingPage = startingPage;
|
||||
this.$root.$emit('show::modal', 'profile');
|
||||
},
|
||||
async getUserGroupPlans () {
|
||||
this.$store.state.groupPlans = await this.$store.dispatch('guilds:getGroupPlans');
|
||||
},
|
||||
27
website/client/components/header/messageCount.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template lang="pug" functional>
|
||||
span.message-count(:class="{'top-count': props.top === true}") {{props.count}}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
position: absolute;
|
||||
right: 0.3em;
|
||||
top: -0.8em;
|
||||
padding: 0.2em;
|
||||
background-color: $red-50;
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +1,17 @@
|
||||
<template lang="pug">
|
||||
div.item-with-icon.item-notifications.dropdown
|
||||
span.message-count.top-count(v-if='notificationsCount > 0') {{ notificationsCount }}
|
||||
.svg-icon.notifications(v-html="icons.notifications")
|
||||
.dropdown-menu.dropdown-menu-right.user-dropdown
|
||||
menu-dropdown.item-notifications(:right="true")
|
||||
div(slot="dropdown-toggle")
|
||||
div
|
||||
message-count(v-if='notificationsCount > 0', :count="notificationsCount", :top="true")
|
||||
.svg-icon.notifications(v-html="icons.notifications")
|
||||
div(slot="dropdown-content")
|
||||
h4.dropdown-item.dropdown-separated(v-if='!hasNoNotifications()') {{ $t('notifications') }}
|
||||
h4.dropdown-item.toolbar-notifs-no-messages(v-if='hasNoNotifications()') {{ $t('noNotifications') }}
|
||||
a.dropdown-item(v-if='user.party.quest && user.party.quest.RSVPNeeded')
|
||||
div {{ $t('invitedTo', {name: quests.quests[user.party.quest.key].text()}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click='questAccept(user.party._id)') Accept
|
||||
button.btn.btn-primary(@click='questReject(user.party._id)') Reject
|
||||
button.btn.btn-primary(@click.stop='questAccept(user.party._id)') Accept
|
||||
button.btn.btn-primary(@click.stop='questReject(user.party._id)') Reject
|
||||
a.dropdown-item(v-if='user.purchased.plan.mysteryItems.length', @click='go("/inventory/items")')
|
||||
span.glyphicon.glyphicon-gift
|
||||
span {{ $t('newSubscriberItem') }}
|
||||
@@ -18,20 +20,19 @@ div.item-with-icon.item-notifications.dropdown
|
||||
span.glyphicon.glyphicon-user
|
||||
span {{ $t('invitedTo', {name: party.name}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click='accept(party, index, "party")') Accept
|
||||
button.btn.btn-primary(@click='reject(party, index, "party")') Reject
|
||||
button.btn.btn-primary(@click.stop='accept(party, index, "party")') Accept
|
||||
button.btn.btn-primary(@click.stop='reject(party, index, "party")') Reject
|
||||
a.dropdown-item(v-if='user.flags.cardReceived', @click='go("/inventory/items")')
|
||||
span.glyphicon.glyphicon-envelope
|
||||
span {{ $t('cardReceived') }}
|
||||
a.dropdown-item(@click='clearCards()', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true')
|
||||
a.dropdown-item(@click.stop='clearCards()')
|
||||
a.dropdown-item(v-for='(guild, index) in user.invitations.guilds')
|
||||
div
|
||||
span.glyphicon.glyphicon-user
|
||||
span {{ $t('invitedTo', {name: guild.name}) }}
|
||||
div
|
||||
button.btn.btn-primary(@click='accept(guild, index, "guild")') Accept
|
||||
button.btn.btn-primary(@click='reject(guild, index, "guild")') Reject
|
||||
button.btn.btn-primary(@click.stop='accept(guild, index, "guild")') Accept
|
||||
button.btn.btn-primary(@click.stop='reject(guild, index, "guild")') Reject
|
||||
a.dropdown-item(v-if='user.flags.classSelected && !user.preferences.disableClasses && user.stats.points',
|
||||
@click='go("/user/profile")')
|
||||
span.glyphicon.glyphicon-plus-sign
|
||||
@@ -40,130 +41,40 @@ div.item-with-icon.item-notifications.dropdown
|
||||
span(@click='navigateToGroup(message.key)')
|
||||
span.glyphicon.glyphicon-comment
|
||||
span {{message.name}}
|
||||
span.clear-button(@click='clearMessages(message.key)', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
|
||||
span.clear-button(@click.stop='clearMessages(message.key)') Clear
|
||||
a.dropdown-item(v-for='notification in groupNotifications', :key='notification.id')
|
||||
span(:class="groupApprovalNotificationIcon(notification)")
|
||||
span {{notification.data.message}}
|
||||
span.clear-button(@click='viewGroupApprovalNotification(notification)', :popover="$t('clear')",
|
||||
popover-placement='right', popover-trigger='mouseenter', popover-append-to-body='true') Clear
|
||||
span.clear-button(@click.stop='viewGroupApprovalNotification(notification)') Clear
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
float: right;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-count.top-count {
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: .5em;
|
||||
padding: .2em;
|
||||
background-color: $red-50;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.item-notifications {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.item-notifications:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
color: $header-color;
|
||||
vertical-align: bottom;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
margin-top: .2em;
|
||||
}
|
||||
|
||||
.item-with-icon:hover {
|
||||
.svg-icon {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* @TODO: Move to shared css */
|
||||
.dropdown:hover .dropdown-menu {
|
||||
display: block;
|
||||
margin-top: 0; // remove the gap so it doesn't close
|
||||
}
|
||||
|
||||
.dropdown + .dropdown {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.dropdown-separated {
|
||||
border-bottom: 1px solid $gray-500;
|
||||
}
|
||||
|
||||
.dropdown-menu:not(.user-dropdown) {
|
||||
background: $purple-200;
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: 0px;
|
||||
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
|
||||
.dropdown-item {
|
||||
font-size: 16px;
|
||||
box-shadow: none;
|
||||
color: $white;
|
||||
border: none;
|
||||
line-height: 1.5;
|
||||
|
||||
&.active {
|
||||
background: $purple-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $purple-300;
|
||||
|
||||
&:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.clear-button {
|
||||
margin-left: .5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import map from 'lodash/map';
|
||||
// import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import quests from 'common/script/content/quests';
|
||||
import notificationsIcon from 'assets/svg/notifications.svg';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
import MessageCount from './messageCount';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuDropdown,
|
||||
MessageCount,
|
||||
},
|
||||
directives: {
|
||||
// bTooltip,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
126
website/client/components/header/userDropdown.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template lang="pug">
|
||||
menu-dropdown.item-user(:right="true")
|
||||
div(slot="dropdown-toggle")
|
||||
div(v-b-tooltip.hover.bottom="$t('user')")
|
||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages", :top="true")
|
||||
.svg-icon.user(v-html="icons.user")
|
||||
.user-dropdown(slot="dropdown-content")
|
||||
a.dropdown-item.edit-avatar.dropdown-separated(@click='showAvatar()')
|
||||
h3 {{ user.profile.name }}
|
||||
span.small-text {{ $t('editAvatar') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='showInbox()')
|
||||
| {{ $t('messages') }}
|
||||
message-count(v-if='user.inbox.newMessages > 0', :count="user.inbox.newMessages")
|
||||
a.dropdown-item(@click='showAvatar("backgrounds", "2017")') {{ $t('backgrounds') }}
|
||||
a.dropdown-item(@click='showProfile("stats")') {{ $t('stats') }}
|
||||
a.dropdown-item(@click='showProfile("achievements")') {{ $t('achievements') }}
|
||||
a.dropdown-item.dropdown-separated(@click='showProfile("profile")') {{ $t('profile') }}
|
||||
router-link.dropdown-item(:to="{name: 'site'}") {{ $t('settings') }}
|
||||
router-link.dropdown-item.dropdown-separated(:to="{name: 'subscription'}") {{ $t('subscription') }}
|
||||
a.nav-link.dropdown-item.dropdown-separated(@click.prevent='logout()') {{ $t('logout') }}
|
||||
li(v-if='!this.user.purchased.plan.customerId', @click='showBuyGemsModal("subscribe")')
|
||||
.dropdown-item.text-center
|
||||
h3.purple {{ $t('needMoreGems') }}
|
||||
span.small-text {{ $t('needMoreGemsInfo') }}
|
||||
img.float-left.align-self-end(src='~assets/images/gem-rain.png')
|
||||
button.btn.btn-primary.btn-lg.learn-button Learn More
|
||||
img.float-right.align-self-end(src='~assets/images/gold-rain.png')
|
||||
</template>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.edit-avatar {
|
||||
h3 {
|
||||
color: $gray-10;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
width: 14.75em;
|
||||
}
|
||||
|
||||
.learn-button {
|
||||
margin: 0.75em 0.75em 0.75em 1em;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
color: $gray-200;
|
||||
font-style: normal;
|
||||
display: block;
|
||||
white-space: normal;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'client/libs/store';
|
||||
import * as Analytics from 'client/libs/analytics';
|
||||
import userIcon from 'assets/svg/user.svg';
|
||||
import MenuDropdown from '../ui/customMenuDropdown';
|
||||
import axios from 'axios';
|
||||
import markPMSRead from 'common/script/ops/markPMSRead';
|
||||
import MessageCount from './messageCount';
|
||||
import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuDropdown,
|
||||
MessageCount,
|
||||
},
|
||||
directives: {
|
||||
bTooltip,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
user: userIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
},
|
||||
methods: {
|
||||
showAvatar (startingPage, subpage) {
|
||||
this.$store.state.avatarEditorOptions.editingUser = true;
|
||||
this.$store.state.avatarEditorOptions.startingPage = startingPage;
|
||||
this.$store.state.avatarEditorOptions.subpage = subpage;
|
||||
this.$root.$emit('show::modal', 'avatar-modal');
|
||||
},
|
||||
showInbox () {
|
||||
markPMSRead(this.user);
|
||||
axios.post('/api/v3/user/mark-pms-read');
|
||||
this.$root.$emit('show::modal', 'inbox-modal');
|
||||
},
|
||||
showProfile (startingPage) {
|
||||
this.$store.state.profileUser = this.user;
|
||||
this.$store.state.profileOptions.startingPage = startingPage;
|
||||
this.$root.$emit('show::modal', 'profile');
|
||||
},
|
||||
showBuyGemsModal (startingPage) {
|
||||
this.$store.state.gemModalOptions.startingPage = startingPage;
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems > User Dropdown',
|
||||
});
|
||||
|
||||
this.$root.$emit('show::modal', 'buy-gems', {alreadyTracked: true});
|
||||
},
|
||||
logout () {
|
||||
this.$store.dispatch('auth:logout');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -79,9 +79,9 @@
|
||||
:showPopover="flatGear[activeItems[group]] && Boolean(flatGear[activeItems[group]].text)",
|
||||
@click="equipItem(flatGear[activeItems[group]])",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
equipmentAttributesPopover(:item="context.item")
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(
|
||||
:selected="true",
|
||||
:show="!costume || user.preferences.costume",
|
||||
@@ -105,7 +105,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: group.label })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:itemContentClass="'shop_' + context.item.key",
|
||||
@@ -113,13 +113,13 @@
|
||||
:key="context.item.key",
|
||||
@click="openEquipDialog(context.item)"
|
||||
)
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(
|
||||
:selected="activeItems[context.item.type] === context.item.key",
|
||||
:show="!costume || user.preferences.costume",
|
||||
@click="equipItem(context.item)",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
equipmentAttributesPopover(:item="context.item")
|
||||
|
||||
equipGearModal(
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:key="context.item.key",
|
||||
@@ -57,10 +57,10 @@
|
||||
|
||||
@click="onEggClicked($event, context.item)",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
.popover-content-text(v-if="currentDraggingPotion == null") {{ context.item.notes }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="context.item.quantity"
|
||||
@@ -74,7 +74,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:key="context.item.key",
|
||||
@@ -88,10 +88,10 @@
|
||||
|
||||
@click="onPotionClicked($event, context.item)"
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
.popover-content-text {{ context.item.notes }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="context.item.quantity"
|
||||
@@ -105,7 +105,7 @@
|
||||
:type="group.key",
|
||||
:noItemsLabel="$t('noGearItemsOfType', { type: $t(group.key) })"
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
item(
|
||||
:item="context.item",
|
||||
:key="context.item.key",
|
||||
@@ -113,7 +113,7 @@
|
||||
:showPopover="currentDraggingPotion == null",
|
||||
@click="itemClicked(group.key, context.item)",
|
||||
)
|
||||
template(slot="popoverContent", scope="context")
|
||||
template(slot="popoverContent", slot-scope="context")
|
||||
div.questPopover(v-if="group.key === 'quests'")
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
questInfo(:quest="context.item")
|
||||
@@ -121,7 +121,7 @@
|
||||
div(v-else)
|
||||
h4.popover-content-title {{ context.item.text }}
|
||||
.popover-content-text(v-html="context.item.notes")
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="context.item.quantity"
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
div(:class="'Pet_Egg_'+item.eggKey")
|
||||
div(v-else)
|
||||
h4.popover-content-title {{ item.name }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(:selected="item.key === currentPet", :show="item.isOwned()", @click="selectPet(item)")
|
||||
|
||||
.btn.btn-flat.btn-show-more(@click="setShowMore(petGroup.key)", v-if='petGroup.key !== "specialPets"')
|
||||
@@ -144,7 +144,7 @@
|
||||
)
|
||||
span(slot="popoverContent")
|
||||
h4.popover-content-title {{ item.name }}
|
||||
template(slot="itemBadge", scope="context")
|
||||
template(slot="itemBadge", slot-scope="context")
|
||||
starBadge(
|
||||
:selected="item.key === currentMount",
|
||||
:show="item.isOwned()",
|
||||
@@ -187,7 +187,7 @@
|
||||
:itemWidth=94,
|
||||
:itemMargin=24,
|
||||
)
|
||||
template(slot="item", scope="context")
|
||||
template(slot="item", slot-scope="context")
|
||||
foodItem(
|
||||
:item="context.item",
|
||||
:itemCount="userItems.food[context.item.key]",
|
||||
|
||||
@@ -20,17 +20,17 @@
|
||||
.is-buffed(v-if="isBuffed")
|
||||
.svg-icon(v-html="icons.buff")
|
||||
span.small-text.character-level {{ characterLevel }}
|
||||
.progress-container
|
||||
.progress-container(b-tooltip.hover.bottom="$t('health')")
|
||||
.svg-icon(v-html="icons.health")
|
||||
.progress
|
||||
.progress-bar.bg-health(:style="{width: `${percent(member.stats.hp, MAX_HEALTH)}%`}")
|
||||
span.small-text {{member.stats.hp | statFloor}} / {{MAX_HEALTH}}
|
||||
.progress-container
|
||||
.progress-container(b-tooltip.hover.bottom="$t('experience')")
|
||||
.svg-icon(v-html="icons.experience")
|
||||
.progress
|
||||
.progress-bar.bg-experience(:style="{width: `${percent(member.stats.exp, toNextLevel)}%`}")
|
||||
span.small-text {{member.stats.exp | statFloor}} / {{toNextLevel}}
|
||||
.progress-container(v-if="hasClass")
|
||||
.progress-container(v-if="hasClass", b-tooltip.hover.bottom="$t('mana')")
|
||||
.svg-icon(v-html="icons.mana")
|
||||
.progress
|
||||
.progress-bar.bg-mana(:style="{width: `${percent(member.stats.mp, maxMP)}%`}")
|
||||
@@ -186,6 +186,7 @@ import Profile from './userMenu/profile';
|
||||
import { toNextLevel } from '../../common/script/statHelpers';
|
||||
import statsComputed from '../../common/script/libs/statsComputed';
|
||||
import percent from '../../common/script/libs/percent';
|
||||
// import bTooltip from 'bootstrap-vue/lib/directives/tooltip';
|
||||
|
||||
import buffIcon from 'assets/svg/buff.svg';
|
||||
import healthIcon from 'assets/svg/health.svg';
|
||||
@@ -198,6 +199,9 @@ export default {
|
||||
Profile,
|
||||
ClassBadge,
|
||||
},
|
||||
directives: {
|
||||
// bTooltip,
|
||||
},
|
||||
props: {
|
||||
member: {
|
||||
type: Object,
|
||||
|
||||
@@ -257,7 +257,7 @@ export default {
|
||||
this.mp(mana);
|
||||
},
|
||||
userLvl (after, before) {
|
||||
if (after <= before || this.isRunningYesterdailies) return;
|
||||
if (after <= before || this.$store.state.isRunningYesterdailies) return;
|
||||
this.showLevelUpNotifications(after);
|
||||
},
|
||||
userClassSelect (after) {
|
||||
@@ -285,7 +285,6 @@ export default {
|
||||
this.$root.$emit('show::modal', 'quest-invitation');
|
||||
},
|
||||
},
|
||||
|
||||
mounted () {
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch'),
|
||||
@@ -345,7 +344,7 @@ export default {
|
||||
this.$root.$emit('playSound', sound);
|
||||
},
|
||||
checkNextCron: throttle(function checkNextCron () {
|
||||
if (!this.isRunningYesterdailies && this.nextCron && Date.now() > this.nextCron) {
|
||||
if (!this.$store.state.isRunningYesterdailies && this.nextCron && Date.now() > this.nextCron) {
|
||||
Promise.all([
|
||||
this.$store.dispatch('user:fetch', {forceLoad: true}),
|
||||
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
|
||||
@@ -367,11 +366,11 @@ export default {
|
||||
|
||||
// Setup a listener that executes 10 seconds after the next cron time
|
||||
this.nextCron = Number(nextCron.format('x'));
|
||||
this.isRunningYesterdailies = false;
|
||||
this.$store.state.isRunningYesterdailies = false;
|
||||
},
|
||||
async runYesterDailies () {
|
||||
if (this.isRunningYesterdailies) return;
|
||||
this.isRunningYesterdailies = true;
|
||||
if (this.$store.state.isRunningYesterdailies) return;
|
||||
this.$store.state.isRunningYesterdailies = true;
|
||||
|
||||
if (!this.user.needsCron) {
|
||||
this.handleUserNotifications(this.user.notifications);
|
||||
@@ -412,7 +411,7 @@ export default {
|
||||
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
|
||||
]);
|
||||
|
||||
if (this.levelBeforeYesterdailies < this.user.stats.lvl) {
|
||||
if (this.levelBeforeYesterdailies > 0 && this.levelBeforeYesterdailies < this.user.stats.lvl) {
|
||||
this.showLevelUpNotifications(this.user.stats.lvl);
|
||||
}
|
||||
|
||||
|
||||
@@ -150,11 +150,11 @@
|
||||
p.benefits(v-markdown='$t("earnGemsMonthly", {cap:45})')
|
||||
p.benefits(v-markdown='$t("receiveMysticHourglasses", {amount:4})')
|
||||
button.btn.btn-primary(@click='subscriptionPlan = "basic_12mo"') {{ subscriptionPlan === "basic_12mo" ? $t('selected') : $t('select') }}
|
||||
.row.text-center
|
||||
.row.text-center(v-if='subscriptionPlan')
|
||||
h2.mx-auto.text-payment {{ $t('choosePaymentMethod') }}
|
||||
.row.text-center
|
||||
a.mx-auto {{ $t('haveCouponCode') }}
|
||||
.card-deck
|
||||
.card-deck(v-if='subscriptionPlan')
|
||||
.card.text-center.payment-method
|
||||
.card-body(@click='showStripe({subscription: subscriptionPlan})')
|
||||
.mx-auto(v-html='icons.creditCard', style='"height: 56px; width: 159px; margin-top: 1em;"')
|
||||
|
||||
@@ -94,12 +94,12 @@ export default {
|
||||
this.user.achievements.streak = clone(this.restoreValues.achievements.streak);
|
||||
|
||||
let settings = {
|
||||
'stats.hp': this.restoreValues.stats.hp,
|
||||
'stats.exp': this.restoreValues.stats.exp,
|
||||
'stats.gp': this.restoreValues.stats.gp,
|
||||
'stats.lvl': this.restoreValues.stats.lvl,
|
||||
'stats.mp': this.restoreValues.stats.mp,
|
||||
'achievements.streak': this.restoreValues.achievements.streak,
|
||||
'stats.hp': Number(this.restoreValues.stats.hp),
|
||||
'stats.exp': Number(this.restoreValues.stats.exp),
|
||||
'stats.gp': Number(this.restoreValues.stats.gp),
|
||||
'stats.lvl': Number(this.restoreValues.stats.lvl),
|
||||
'stats.mp': Number(this.restoreValues.stats.mp),
|
||||
'achievements.streak': Number(this.restoreValues.achievements.streak),
|
||||
};
|
||||
|
||||
this.$store.dispatch('user:set', settings);
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
:popoverPosition="'top'",
|
||||
@click="featuredItemSelected(item)"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -101,7 +101,7 @@
|
||||
:type="'gear'",
|
||||
:noItemsLabel="$t('noGearItemsOfClass')"
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
shopItem(
|
||||
:key="ctx.item.key",
|
||||
:item="ctx.item",
|
||||
@@ -110,7 +110,7 @@
|
||||
@click="gearSelected(ctx.item)"
|
||||
)
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -151,13 +151,13 @@
|
||||
strong(v-if='item.key === "gem" && gemsLeft === 0') {{ $t('maxBuyGems') }}
|
||||
h4.popover-content-title {{ item.text }}
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
countBadge(
|
||||
v-if="item.showCount != false",
|
||||
:show="userItems[item.purchaseType][item.key] != 0",
|
||||
:count="userItems[item.purchaseType][item.key] || 0"
|
||||
)
|
||||
.gems-left(v-if='item.key === "gem"')
|
||||
.badge.badge-pill.badge-purple.gems-left(v-if='item.key === "gem"')
|
||||
| {{ gemsLeft }}
|
||||
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
@@ -196,14 +196,14 @@
|
||||
:itemWidth=94,
|
||||
:itemMargin=24,
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
item(
|
||||
:item="ctx.item",
|
||||
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
|
||||
popoverPosition="top",
|
||||
@click="selectedItemToSell = ctx.item"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
|
||||
@@ -218,13 +218,13 @@
|
||||
:text="selectedItemToSell != null ? getItemName(selectedDrawerItemType, selectedItemToSell) : ''",
|
||||
@change="resetItemToSell($event)"
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
item.flat(
|
||||
:item="ctx.item",
|
||||
:itemContentClass="getItemClass(selectedDrawerItemType, ctx.item.key)",
|
||||
:showPopover="false"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
countBadge(
|
||||
:show="true",
|
||||
:count="userItems[drawerTabs[selectedDrawerTab].contentType][ctx.item.key] || 0"
|
||||
@@ -356,17 +356,8 @@
|
||||
}
|
||||
|
||||
.market .gems-left {
|
||||
position: absolute;
|
||||
right: -.5em;
|
||||
top: -.5em;
|
||||
color: $white;
|
||||
background: $purple-200;
|
||||
padding: .15em;
|
||||
text-align: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 1px 0 rgba($black, 0.12);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -54,12 +54,12 @@
|
||||
:popoverPosition="'top'",
|
||||
@click="selectItem(item)"
|
||||
)
|
||||
template(slot="popoverContent", scope="ctx")
|
||||
template(slot="popoverContent", slot-scope="ctx")
|
||||
div.questPopover
|
||||
h4.popover-content-title {{ item.text }}
|
||||
questInfo(:quest="item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -94,7 +94,7 @@
|
||||
:itemMargin=24,
|
||||
:type="'pet_quests'",
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
shopItem(
|
||||
:key="ctx.item.key",
|
||||
:item="ctx.item",
|
||||
@@ -104,12 +104,12 @@
|
||||
:emptyItem="false",
|
||||
@click="selectItem(ctx.item)"
|
||||
)
|
||||
span(slot="popoverContent", scope="ctx")
|
||||
span(slot="popoverContent", slot-scope="ctx")
|
||||
div.questPopover
|
||||
h4.popover-content-title {{ ctx.item.text }}
|
||||
questInfo(:quest="ctx.item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -145,7 +145,7 @@
|
||||
.popover-content-text(v-if='item.lvl > user.stats.lvl') {{ `${$t('mustLvlQuest', {level: item.lvl})}` }}
|
||||
questInfo(v-if='!item.locked', :quest="item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -172,7 +172,7 @@
|
||||
h4.popover-content-title {{ item.text }}
|
||||
questInfo(:quest="item")
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
@@ -190,7 +190,7 @@
|
||||
:withPin="true",
|
||||
@change="resetItemToBuy($event)",
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
item.flat(
|
||||
:item="ctx.item",
|
||||
:itemContentClass="ctx.item.class",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
@click="itemSelected(item)"
|
||||
)
|
||||
|
||||
h1.mb-0.page-header(v-once) {{ $t('seasonalShop') }}
|
||||
h1.mb-0.page-header(v-once, v-if='seasonal.opened') {{ $t('seasonalShop') }}
|
||||
|
||||
.clearfix(v-if="seasonal.opened")
|
||||
h2.float-left
|
||||
@@ -97,7 +97,7 @@
|
||||
:showEventBadge="false",
|
||||
@click="itemSelected(item)"
|
||||
)
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
@click.prevent.stop="togglePinned(ctx.item)"
|
||||
|
||||
@@ -18,8 +18,7 @@ div
|
||||
slot(name="itemImage", :item="item")
|
||||
|
||||
div.price
|
||||
span.svg-icon.inline.icon-16(v-html="icons[currencyClass]")
|
||||
|
||||
span.svg-icon.inline.icon-16(v-html="icons[currencyClass]", v-once)
|
||||
span.price-label(:class="currencyClass", v-once) {{ getPrice() }}
|
||||
b-popover(
|
||||
:target="itemId",
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
:itemMargin=24,
|
||||
:type="category.identifier",
|
||||
)
|
||||
template(slot="item", scope="ctx")
|
||||
template(slot="item", slot-scope="ctx")
|
||||
shopItem(
|
||||
:key="ctx.item.key",
|
||||
:item="ctx.item",
|
||||
@@ -72,11 +72,11 @@
|
||||
:emptyItem="false",
|
||||
@click="selectItemToBuy(ctx.item)"
|
||||
)
|
||||
span(slot="popoverContent", scope="ctx")
|
||||
span(slot="popoverContent", slot-scope="ctx")
|
||||
div
|
||||
h4.popover-content-title {{ ctx.item.text }}
|
||||
|
||||
template(slot="itemBadge", scope="ctx")
|
||||
template(slot="itemBadge", slot-scope="ctx")
|
||||
span.badge.badge-pill.badge-item.badge-svg(
|
||||
v-if="ctx.item.pinType !== 'IGNORE'",
|
||||
:class="{'item-selected-badge': ctx.item.pinned, 'hide': !ctx.item.pinned}",
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
.align-self-center.right-margin(:class='baileyClass')
|
||||
.media-body
|
||||
h1.align-self-center(v-markdown='$t("newStuff")')
|
||||
h2 10/26/2017 - BEHIND THE SCENES: THE JOYFUL REAPER
|
||||
h2 11/7/2017 - THUNDERSTORM HATCHING POTIONS AND COSTUME CHALLENGE AWARDS
|
||||
hr
|
||||
.media
|
||||
.media-body
|
||||
h3 Behind the Scenes Blog Post: The Joyful Reaper
|
||||
p(v-markdown='"Have you ever wanted to know more about the Joyful Reaper, the Master of Healers? [Today\'s blog post](https://habitica.wordpress.com/2017/10/26/behind-the-scenes-spotlight-on-the-joyful-reaper/) features a spotlight on the guardian of the Flourishing Fields! Check it out now to learn about her masterful healer skills, as well as an explanation of some of Habitica\'s quirks."')
|
||||
.small by Lemoness
|
||||
.npc_joyful_reaper
|
||||
h3 Thunderstorm Hatching Potions
|
||||
p(v-markdown='"There\'s a new pet breed in town! Between now and November 30th, you can buy Thunderstorm Hatching Potions from [the Market](/shops/market) and use them to hatch any standard pet egg. (Magic Hatching Potions do not work on Quest Pet eggs.) Thunderstorm Potion Pets aren\'t picky, so they\'ll happily eat any kind of food that you feed them!"')
|
||||
p After they're gone, it will be at least a year before the Thunderstorm Hatching Potions are available again, so be sure to get them now!
|
||||
.small by Balduranne and SabreCat
|
||||
h3 Costume Challenge Awarded
|
||||
p(v-markdown='"Congratulations to everyone who completed this year\'s costume Challenge! If your entry followed the rules, you\'ve been awarded your badge (or had your badge count increased). (If you have any issues, email admin@habitica.com and we will investigate for you). We\'ll be sharing some of our favorites to our [Tumblr](http://blog.habitrpg.com/), so be sure to follow us to see all the great entries!"')
|
||||
.small by Lemoness, SabreCat, and Beffymaroo
|
||||
.promo_potions_thunderstorm
|
||||
br
|
||||
</template>
|
||||
|
||||
|
||||