Compare commits

..

83 Commits

Author SHA1 Message Date
thehollidayinn 95f9479d7a 4.12.6 2017-12-02 08:37:36 -06:00
Keith Holliday af095d8450 Revert query optimization (#9636) 2017-12-02 08:35:31 -06:00
Sabe Jones 470495387c 4.12.5 2017-12-02 03:28:44 +00:00
Keith Holliday bdef1ca23c Fixed max width none (#9631) 2017-12-01 21:26:52 -06:00
SabreCat 1835804e86 4.12.4 2017-12-01 21:33:06 +00:00
SabreCat cb58994bdf Merge branch 'release' into develop 2017-12-01 21:32:28 +00:00
SabreCat 44f3b73183 fix(avatar): layer base/bangs correctly 2017-12-01 21:10:33 +00:00
Luan Muniz 7e23fdc22a Fix install node permissions (#9621)
Signed-off-by: Luan <luan@luanmuniz.com.br>
2017-12-01 16:05:17 +00:00
Keith Holliday e138d2b67b Added needs cron check to achievements (#9624) 2017-12-01 09:54:43 -06:00
Keith Holliday 3e3248fecb Changed row adding when user blurs/focuses (#9610) 2017-12-01 08:37:37 -06:00
Keith Holliday 78ee60611a Remove cancel button when clicked (#9616) 2017-12-01 08:03:54 -06:00
Keith Holliday 3c7aaa605b Fixed text being cut off (#9612) 2017-12-01 08:03:36 -06:00
Keith Holliday 00343da266 Added max width seetings to screens larger than 1300 (#9609) 2017-12-01 07:47:45 -06:00
Sabe Jones 56d09411d9 Merge branch 'release' into develop 2017-12-01 00:31:23 +00:00
Sabe Jones ae0df2242a 4.12.3 2017-12-01 00:30:49 +00:00
Sabe Jones 5b06b28c97 chore(event): end Thunderstorm Potions 2017-12-01 00:30:06 +00:00
Keith Holliday c6a3bfb291 Added exp reset when changing level (#9611) 2017-11-30 15:45:26 -06:00
Keith Holliday 7797794cd5 Deselect a tag if it is selected when removing (#9614) 2017-11-30 15:45:14 -06:00
Keith Holliday d9e09a5f3d Fixed streak bonus style (#9608) 2017-11-30 12:37:53 -06:00
Keith Holliday 4e73c8513e Hide progress if user is not on quest (#9597) 2017-11-30 12:37:31 -06:00
Keith Holliday 9421fd7ced Added analytics to backgrounds (#9615) 2017-11-30 12:10:49 -06:00
Keith Holliday 699de64328 Added more fields to scoring (#9613) 2017-11-30 10:09:04 -06:00
Keith Holliday 6f9cbf9ca1 Only update the user when editing profile (#9601) 2017-11-30 08:19:03 -06:00
Keith Holliday a097819b72 User auth performance improvements (#9589)
* Added initial user projecting in auth and fixed projection for get user tasks

* Added fields to score route

* Added another field to get tasks

* Added group fields to user
2017-11-30 08:17:28 -06:00
Keith Holliday 77f71b5415 Fixed saving in progress tag when clicking save (#9598) 2017-11-30 08:16:54 -06:00
Keith Holliday ced3621dea Fixed leaving from guild list item (#9599) 2017-11-30 08:16:00 -06:00
Keith Holliday e321d85b3c Donate buy modal fix (#9604)
* Added donate back and buy modal

* Fixed login check

* Added ability to remove mustache
2017-11-30 08:15:28 -06:00
SabreCat d72b40d5b0 Merge branch 'release' into develop 2017-11-29 05:07:42 +00:00
SabreCat 54443a2980 4.12.2 2017-11-29 05:04:46 +00:00
SabreCat 00dc990974 chore(event): end Thanksgiving, add Bailey 2017-11-29 05:03:01 +00:00
Keith Holliday 3737aa045d Fixed text when cloning (#9594) 2017-11-28 19:18:04 -06:00
Keith Holliday b03ddf6f7d Added fix for task order using / for arrays (#9590) 2017-11-28 14:57:08 -06:00
tim1234ltp 4ab89fd3e0 Bug fixes on Subscription termination date format [Fixes Issues #9186] (#9583)
* Fixed date.

* Got rid of the filter and returned moment.

* fix the return value

* Stupid typo.
2017-11-28 09:19:29 +01:00
negue f1e200c0f5 autofix pinned seasonal gear - fixes #9448 (#9570)
* auto-remove officialPinned item from userPinned-array on pinning

* hide event limited message if an item was already owned by the user
2017-11-28 09:11:40 +01:00
Ryan Holinshead 218664dfcc (ISSUE-9353) Fix pinned item alignment (#9358)
* (ISSUE-9353) Fix pinned item alignment
- Get rid of justify-content: space-between to allow flex-start default to be used
- Add margin to direct children (item-wrappers)

* Issue-9353: Change selector to & > div instead of just > for more explicit selection of direct child divs

* Fix rewards item spacing to match Zeplin mockups
- Make sure horizontal/vertical spacing between items is 16px
- Add use of grid if supported, else use flex
2017-11-27 20:54:03 -06:00
Trevor Ford a0f29e970d fix Stable sidebar width and center inventory drawer (fixes #9263) (#9419)
* fix Stable sidebar width and center inventory drawer (fixes #9263)

* hide all .standard-sidebars on small/mobile devices
2017-11-27 20:48:52 -06:00
MathWhiz 200cd66d66 Use config when starting development server (#9410)
* Use config when starting development server

* import nconf setup from website

* Add comment explaining choice

* Fix lint issues
2017-11-27 20:38:27 -06:00
MathWhiz dd05a8d608 Contributor Title tooltip (#9413)
* Remove usage of cachedProfileData when determining contributor level

* Add tooltip

* Remove directive import

* /s/msg.contributor.title/msg.contributor.text

* move tooltip placement

* update tooltip position
2017-11-27 20:37:36 -06:00
Asif Mallik 299e88233c Fixes multiple complete and uncomplete for todos and daily (Fixes #8669) (#8971)
* Fixed bug that allows users to complete todo and daily multiple times

* Added tests

* Fix syntax

* Fix existing tests that rely on multiple complete or uncomplete

* Undoes removal of website/client/README.md

* Change sessionOutdated string to reflect separate client needs

* Fix should update history test by changing lastCron
2017-11-27 20:13:18 -06:00
Ryan Holinshead 26bde1f766 Character Create Modal - Prevent Options From Jumping When Selected (#9252)
* Prevent options from jumping when selected due to border being added/removed based on active option. Instead, always have a border on the option but set its color when active

* Use gray instead of white border in order to match background so that it isn't visible while unselected. Add margin-bottom back

* Make sure the locked option style remains unchanged
- Nest .locked in .customize-options .option to get specificity
- Override border, border-radius, and margin-top for .locked
- Set the override values to what would be applied without other style changes
2017-11-27 20:11:15 -06:00
Garrett Scott d95836b881 Translator minor changes fixes #8917 (#9297)
* Updated userItemsNotEnough string

* Added a variable to be passed to the deleteSocialAccountText string. This variable name is `magic_word` and is set as DELETE where used

* modified incorrectDeletePhrase to use a variable rather than translatable string for the word DELETE. Updated the DELETE-user test and the user api

* Changed noSudoAccess from translatable string to static

* Changed enterprisePlansEmailSubject from a translatable string to a static string within groupPlans.vue

* Fixed test problems with translation fixes

* Added no sudo access string to api messages

* changed plain string to apiMessage for no sudo access messages
2017-11-27 20:08:39 -06:00
Tyler Nychka fac81bb9ee Long names overflow task box fixes #9403 (#9404)
* Issue 9403 Long names overflow task box

* Added padding

* Enabled overflow-wrap: break-word;

Added min width to allow overflow-wrap to actually break content
2017-11-27 20:07:13 -06:00
Sarvesh Kakodkar b323abd225 Added confirmation step at begin button for quest (#9199)
* Added confirmation step at begin button for quest

* Fixed the 3 errors caused by questConfirm method in travis-ci
2017-11-27 20:05:05 -06:00
negue b3870e5f34 multiple market fixes (#9468)
* show `selectMemberModal` to send a card, even if the user doesn't have a party yet

* market - prevent filter reset on pinning items

* hide buy amount for gear, backgrounds, mystery_set, card, rebirth_orb, fortify, armoire - fix mystery set preview in timetravelers

* purchase confirmation on gem / hourglass purchases

* fix lint
2017-11-27 19:54:55 -06:00
kartik adur 29dc56c12f Party roster sorter: Member Modal Component (#9472)
* modify sort options for party members

* add unittest for membersModalComponent sort

* updates as requested in PR

* removed duplicates for `class` and `background` from flavour text

* fix linting error thrown by travis ci
2017-11-27 19:54:13 -06:00
Esben Sparre Andreasen b62f08d500 Misc. bug fixes from lgtm.com (2) (#9474)
* Remove dead branch of ternary: `gift` is always truthy here

Problem found here:

- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/server/libs/amazonPayments.js?sort=name&dir=ASC&mode=heatmap&excluded=false#x5a22f31110a55091:1

* Remove superfluous argument, preenUserHistory only takes two args

Problem found here:

- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/server/libs/cron.js?sort=name&dir=ASC&mode=heatmap&excluded=false#xf16a045ecabb07f6:1

* Cleanup: remove useless assignments

Problems found here:

- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/store/actions/shops.js?sort=name&dir=ASC&mode=heatmap&excluded=false#xf782ed2cf920441%3A1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/app.vue?sort=name&dir=ASC&mode=heatmap&excluded=false#x172c1dda85e84dc8%3A1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/components/settings/site.vue#x9b3afee802a3a8f8%3A1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/client/components/selectMembersModal.vue?sort=name&dir=ASC&mode=heatmap&excluded=false#x1fbc2a3d62facd70:1
- https://lgtm.com/projects/g/HabitRPG/habitrpg/snapshot/dist-98076885-1510577633582/files/website/common/script/libs/taskClasses.js?sort=name&dir=ASC&mode=heatmap&excluded=false#x41ce0e121a4defee:1

* Fix online editor whitespace change.
2017-11-27 19:51:25 -06:00
Esben Sparre Andreasen f62177fb1a Misc. bug fixes from lgtm.com (#9325)
* Bugfix: declare variable locally

* Bugfix: fix syntax error

* Bugfix: regex char-class with alternatives

The old implementation used character classes instead of
alternatives. As a consequence, the regex would match:

- a_warrior_0
- r_warrior_0
- m_warrior_0
- o_warrior_0
- r_warrior_0
- |_warrior_0
- h_warrior_0
- ...

The regex will now match:

- armor_warrior_0
- head_warrior_0
- shield_warrior_0
2017-11-27 19:51:02 -06:00
Paul 885f2998ae System messages flaggable (#9408)
* Remove flag from system messages, throw an error if system messages are flagged

* Modify unflag system message test to check if flagging a system message throws an error

* Move email from nconf to top
2017-11-27 19:45:04 -06:00
Paul 2afd96e11c Remove spaces from filters (#9524) 2017-11-27 19:44:33 -06:00
Paul cd92f44365 Fix difficult to edit checklists in Firefox (#9525)
* Change checklist item hover from move to text

* Add vue draggable

* Add vue draggable

* Replace sortablejs directive with Vue Draggable component

* Indent draggable properties
2017-11-27 19:43:24 -06:00
Paul 863177902a Remove extra 'to' paramater from router link (#9529) 2017-11-27 19:42:53 -06:00
Cassidy Pignatello 96974461e5 Change badge tooltips fixes https://github.com/HabitRPG/habitica/issues/9520 (#9539)
* updates text for costume contestant

* updates text for contributor badge

* removes unnecessary "Badge" text from contribName
2017-11-27 19:37:18 -06:00
Paul 8895b70ffa Adjust left positioning of the left-panel to unobscure sroll-bar, change left panel overflow to overflow-y (#9544) 2017-11-27 19:35:30 -06:00
Kip Raske 03480ebfc7 Passing quantity to the API call when buying quests from the shop (#9565)
You can buy multiple quests from the buy modal, but the quanitity is
never passed to the server. So the client thinks that you are buying the
number you said you did, but the server only buys one regardless. This
can lead to syncing problems down the road.
2017-11-27 19:33:08 -06:00
Joseti 9b8676f02e Changed "Donate" button to "Contribute" button (#9581)
* Changed functionality of "donate"-button on static pages

* Changed strings to reflect change from "donate" to "contribute"
2017-11-27 19:29:45 -06:00
Keith Holliday 3e7738b5b1 Added keys to for loops (#9584) 2017-11-27 12:46:25 -06:00
Keith Holliday 33a235b46c Fixed equiping glasses and ears (#9585) 2017-11-27 12:19:36 -06:00
Keith Holliday 137d6c1f9d Set default empty array when party members haven't loaded (#9579)
* Set default empty array when party members haven't loaded

* Corrected variable usage
2017-11-27 11:59:42 -06:00
Keith Holliday 1a5e820d88 Fixed when user unclaims a task assigned to them (#9578)
* Fixed when user unclaims a task assigned to them

* Removed test

* Fixed lint
2017-11-27 11:38:59 -06:00
Keith Holliday 0c7f9ca6bb Changed search to regex (#9575)
* Changed search to regex

* Changed  to array
2017-11-27 11:10:26 -06:00
Keith Holliday 3e6b3ce3ff Added habitica event for profile display (#9576) 2017-11-27 10:29:20 -06:00
Keith Holliday ea5ba965e7 Fixed promoting group leader (#9574) 2017-11-27 10:27:30 -06:00
Keith Holliday 7215a550b5 Added page increment when page is loaded to prevent scroll stopping (#9573) 2017-11-27 10:23:04 -06:00
Keith Holliday 3235dfa236 Added equipment filter (#9572) 2017-11-27 10:22:34 -06:00
Keith Holliday 9baf7a7c67 Do not reset item when buying cards (#9571) 2017-11-27 10:22:00 -06:00
Alys cd629ef7fa add missing @ before a contributor's name in quest content 2017-11-27 09:16:13 +00:00
Keith Holliday 9ef7c45241 Ensured user is saved after validation checks (#9569) 2017-11-23 20:46:02 -06:00
Alys fef3d09f2d remove words related to alcohol
This is because they're causing problems in housework guilds (alcohol
is used for cleaning) and since it's coming up to Christmas a lot of
guilds are starting to have permissible conversations about drinks,
rum balls, etc.

We might put these back after Christmas but make specific exceptions
for the housework guilds.

When a more advanced word blocker is introduced, we'll be able to
ban alcohol words only in the Tavern. For irony.
2017-11-23 17:52:22 +00:00
Matteo Pagliazzi 53c83c585a fix navbar not showing up in homepage 2017-11-23 16:07:58 +01:00
Keith Holliday e628c5dc3b Fixed task best color (#9563) 2017-11-21 14:02:53 -06:00
Keith Holliday 9eaa531f66 Amazon payment fixes (#9562)
* Added custom amazon event, removed redundency, fixed variable names

* Fixed more variables and group plan data
2017-11-21 14:02:40 -06:00
Keith Holliday 3ffea4332e Added extra confirmation incase the class modal shows multiple times (#9557) 2017-11-20 15:57:33 -06:00
Sabe Jones 4618fd8954 Merge branch 'release' into develop 2017-11-20 19:21:17 +00:00
Sabe Jones 791c19b5f1 4.12.1 2017-11-20 19:20:54 +00:00
Sabe Jones 7193cc6bae chore(i18n): update locales 2017-11-20 19:20:03 +00:00
Keith Holliday 1845bd1e35 Fixed display of RYA behind bailey (#9555) 2017-11-20 12:38:26 -06:00
Keith Holliday 5f468d16b7 Cancel users free group plan when they leave a group (#9543)
* Cancel users free group plan when they leave a group

* Fixed lint
2017-11-20 12:34:41 -06:00
Keith Holliday 20a99e526d Should do diff week fix (#9551)
* Fixed week difference when changing year

* Added year switchover test

* Fixed start date setting
2017-11-20 12:22:01 -06:00
Keith Holliday 1e69f42d0f Added account transfer migration (#9548)
* Added account transfer migration

* Removed bad comment
2017-11-19 16:53:08 -06:00
Keith Holliday 9c2f5213cb Challenge fixes (#9528)
* Added challenge member search to progress dropdown

* Added leave challenge modal

* Allowed editing for challenge leader only

* Pevented users from editing challenge task info

* Set default progress default to daily

* Removed reward filters from user challenge progress
2017-11-17 17:13:07 -06:00
Sabe Jones c06d5107ac Merge branch 'release' into develop 2017-11-17 22:50:27 +00:00
MathWhiz 89e4cbcffe Disable immutable inputs when editing a challenge (#9412)
* Disable uneditable inputs when editing a challenge

* Revert prize display when creating
2017-11-17 23:45:38 +11:00
Keith Holliday 67564317fb Group plan fixes (#9518)
* Fixed group plan editing

* Added translations

* Abstracted query for group or challenge tasks
2017-11-17 20:31:39 +11:00
160 changed files with 1817 additions and 1499 deletions
@@ -16,7 +16,7 @@ var migrationName = '20140831_increase_gems_for_previous_contributions';
* https://github.com/HabitRPG/habitrpg/issues/3933
* Increase Number of Gems for Contributors
* author: Alys (d904bd62-da08-416b-a816-ba797c9ee265)
*
*
* Increase everyone's gems per their contribution level.
* Originally they were given 2 gems per tier.
* Now they are given 3 gems per tier for tiers 1,2,3
@@ -70,7 +70,7 @@ dbUsers.findEach(query, fields, function(err, user) {
var extraGems = tier; // tiers 1,2,3
if (tier > 3) { extraGems = 3 + (tier - 3) * 2; }
if (tier == 8) { extraGems = 11; }
extraBalance = extraGems / 4;
var extraBalance = extraGems / 4;
set['balance'] = user.balance + extraBalance;
// Capture current state of user:
@@ -39,7 +39,7 @@ function findUsers(gt){
console.log('User: ', countUsers, user._id);
var update = {
$set: {};
$set: {}
};
if(user.auth && user.auth.local) {
@@ -60,4 +60,4 @@ function findUsers(gt){
});
};
findUsers();
findUsers();
+7 -3
View File
@@ -17,8 +17,12 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
var processUsers = require('./groups/update-groups-with-group-plans');
const processUsers = require('./users/account-transfer');
processUsers()
.catch(function (err) {
console.log(err)
.then(() => {
process.exit();
})
.catch(function (err) {
console.log(err);
process.exit();
});
+38
View File
@@ -0,0 +1,38 @@
var migrationName = 'AccountTransfer';
var authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
var authorUuid = ''; //... own data is done
/*
* This migraition will copy user data from prod to test
*/
const monk = require('monk');
const connectionString = '';
const Users = monk(connectionString).get('users', { castIds: false });
import uniq from 'lodash/uniq';
import Bluebird from 'bluebird';
module.exports = async function accountTransfer () {
const fromAccountId = '';
const toAccountId = '';
const fromAccount = await Users.findOne({_id: fromAccountId});
const toAccount = await Users.findOne({_id: toAccountId});
const newMounts = Object.assign({}, fromAccount.items.mounts, toAccount.items.mounts);
const newPets = Object.assign({}, fromAccount.items.pets, toAccount.items.pets);
const newBackgrounds = Object.assign({}, fromAccount.purchased.background, toAccount.purchased.background);
await Users.update({_id: toAccountId}, {
$set: {
'items.pets': newPets,
'items.mounts': newMounts,
'purchased.background': newBackgrounds,
},
})
.then((result) => {
console.log(result);
});
};
+391 -521
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.12.0",
"version": "4.12.6",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -122,6 +122,7 @@
"vue-router": "^3.0.0",
"vue-style-loader": "^3.0.0",
"vue-template-compiler": "^2.5.2",
"vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker#825a866b6a9c52dd8c588a3e8b900880875ce914",
"webpack": "^2.2.1",
"webpack-merge": "^4.0.0",
@@ -3,6 +3,7 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import config from '../../../../../config.json';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:id/chat/:id/clearflags', () => {
@@ -74,7 +75,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
expect(messages[0].flagCount).to.eql(0);
});
it('can unflag a system message', async () => {
it('can\'t flag a system message', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
type: 'party',
@@ -95,13 +96,15 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
await member.post('/user/class/cast/mpheal');
let [skillMsg] = await member.get(`/groups/${group.id}/chat`);
await member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`);
await admin.post(`/groups/${group._id}/chat/${skillMsg.id}/clearflags`);
let messages = await members[0].get(`/groups/${group._id}/chat`);
expect(messages[0].id).to.eql(skillMsg.id);
expect(messages[0].flagCount).to.eql(0);
await expect(member.post(`/groups/${group._id}/chat/${skillMsg.id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS.COMMUNITY_MANAGER_EMAIL}),
});
// let messages = await members[0].get(`/groups/${group._id}/chat`);
// expect(messages[0].id).to.eql(skillMsg.id);
// expect(messages[0].flagCount).to.eql(0);
});
});
@@ -1,8 +1,8 @@
import {
generateUser,
translate as t,
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('GET /coupons/', () => {
let user;
@@ -19,7 +19,7 @@ describe('GET /coupons/', () => {
await expect(user.get('/coupons')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noSudoAccess'),
message: apiMessages('noSudoAccess'),
});
});
@@ -4,6 +4,7 @@ import {
resetHabiticaDB,
} from '../../../../helpers/api-v3-integration.helper';
import couponCode from 'coupon-code';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('POST /coupons/generate/:event', () => {
let user;
@@ -25,7 +26,7 @@ describe('POST /coupons/generate/:event', () => {
await expect(user.post('/coupons/generate/aaa')).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('noSudoAccess'),
message: apiMessages('noSudoAccess'),
});
});
@@ -10,6 +10,8 @@ import { v4 as generateUUID } from 'uuid';
import {
each,
} from 'lodash';
import { model as User } from '../../../../../website/server/models/user';
import * as payments from '../../../../../website/server/libs/payments';
describe('POST /groups/:groupId/leave', () => {
let typesOfGroups = {
@@ -264,4 +266,45 @@ describe('POST /groups/:groupId/leave', () => {
expect(userWithNonExistentParty.party).to.eql({});
});
});
context('Leaving a group plan', () => {
it('cancels the free subscription', async () => {
// Create group
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Private Guild',
type: 'guild',
},
members: 1,
});
let leader = groupLeader;
let member = members[0];
let userWithFreePlan = await User.findById(leader._id).exec();
// Create subscription
let paymentData = {
user: userWithFreePlan,
groupId: group._id,
sub: {
key: 'basic_3mo',
},
customerId: 'customer-id',
paymentMethod: 'Payment Method',
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
};
await payments.createSubscription(paymentData);
await member.sync();
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
expect(member.purchased.plan.dateTerminated).to.not.exist;
// Leave
await member.post(`/groups/${group._id}/leave`);
await member.sync();
expect(member.purchased.plan.dateTerminated).to.exist;
});
});
});
@@ -130,6 +130,7 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('uncompletes todo when direction is down', async () => {
await user.post(`/tasks/${todo._id}/score/up`);
await user.post(`/tasks/${todo._id}/score/down`);
let updatedTask = await user.get(`/tasks/${todo._id}`);
@@ -137,9 +138,23 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedTask.dateCompleted).to.be.a('undefined');
});
it('scores up todo even if it is already completed'); // Yes?
it('doesn\'t let a todo be completed twice', async () => {
await user.post(`/tasks/${todo._id}/score/up`);
await expect(user.post(`/tasks/${todo._id}/score/up`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
it('scores down todo even if it is already uncompleted'); // Yes?
it('doesn\'t let a todo be uncompleted twice', async () => {
await expect(user.post(`/tasks/${todo._id}/score/down`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
context('user stats when direction is up', () => {
let updatedUser;
@@ -163,23 +178,25 @@ describe('POST /tasks/:id/score/:direction', () => {
});
context('user stats when direction is down', () => {
let updatedUser;
let updatedUser, initialUser;
beforeEach(async () => {
await user.post(`/tasks/${todo._id}/score/up`);
initialUser = await user.get('/user');
await user.post(`/tasks/${todo._id}/score/down`);
updatedUser = await user.get('/user');
});
it('decreases user\'s mp', () => {
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
});
it('decreases user\'s exp', () => {
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
});
it('decreases user\'s gold', () => {
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
});
});
});
@@ -202,6 +219,7 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('uncompletes daily when direction is down', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
await user.post(`/tasks/${daily._id}/score/down`);
let task = await user.get(`/tasks/${daily._id}`);
@@ -222,9 +240,22 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(task.nextDue.length).to.eql(6);
});
it('scores up daily even if it is already completed'); // Yes?
it('doesn\'t let a daily be completed twice', async () => {
await user.post(`/tasks/${daily._id}/score/up`);
await expect(user.post(`/tasks/${daily._id}/score/up`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
it('scores down daily even if it is already uncompleted'); // Yes?
it('doesn\'t let a daily be uncompleted twice', async () => {
await expect(user.post(`/tasks/${daily._id}/score/down`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('sessionOutdated'),
});
});
context('user stats when direction is up', () => {
let updatedUser;
@@ -248,23 +279,25 @@ describe('POST /tasks/:id/score/:direction', () => {
});
context('user stats when direction is down', () => {
let updatedUser;
let updatedUser, initialUser;
beforeEach(async () => {
await user.post(`/tasks/${daily._id}/score/up`);
initialUser = await user.get('/user');
await user.post(`/tasks/${daily._id}/score/down`);
updatedUser = await user.get('/user');
});
it('decreases user\'s mp', () => {
expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp);
expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp);
});
it('decreases user\'s exp', () => {
expect(updatedUser.stats.exp).to.be.lessThan(user.stats.exp);
expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp);
});
it('decreases user\'s gold', () => {
expect(updatedUser.stats.gp).to.be.lessThan(user.stats.gp);
expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp);
});
});
});
@@ -82,6 +82,13 @@ describe('POST /tasks/:id/score/:direction', () => {
});
it('should update the history', async () => {
let newCron = new Date(2015, 11, 20);
await user.post('/debug/set-cron', {
lastCron: newCron,
});
await user.post('/cron');
await user.post(`/tasks/${usersChallengeTaskId}/score/up`);
let tasks = await user.get(`/tasks/challenge/${challenge._id}`);
@@ -75,15 +75,6 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
});
});
it('returns error when non leader tries to create a task', async () => {
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
it('unassigns a user from a task', async () => {
await user.post(`/tasks/${task._id}/unassign/${member._id}`);
@@ -129,4 +120,26 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
it('allows a user to unassign themselves', async () => {
await member.post(`/tasks/${task._id}/unassign/${member._id}`);
let groupTask = await user.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
// @TODO: Which do we want? The user to unassign themselves or not. This test was in
// here, but then we had a request to allow to unaissgn.
xit('returns error when non leader tries to unassign their a task', async () => {
await expect(member.post(`/tasks/${task._id}/unassign/${member._id}`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanEditTasks'),
});
});
});
@@ -308,7 +308,7 @@ describe('DELETE /user', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('incorrectDeletePhrase'),
message: t('incorrectDeletePhrase', {magicWord: 'DELETE'}),
});
});
@@ -7,6 +7,7 @@ import {
import i18n from '../../../../../website/common/script/i18n';
import { ensureAdmin, ensureSudo } from '../../../../../website/server/middlewares/ensureAccessRight';
import { NotAuthorized } from '../../../../../website/server/libs/errors';
import apiMessages from '../../../../../website/server/libs/apiMessages';
describe('ensure access middlewares', () => {
let res, req, next;
@@ -42,7 +43,7 @@ describe('ensure access middlewares', () => {
ensureSudo(req, res, next);
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noSudoAccess')));
expect(next).to.be.calledWith(new NotAuthorized(apiMessages('noSudoAccess')));
});
it('passes when user is a sudo user', () => {
@@ -0,0 +1,31 @@
import Vue from 'vue';
import MembersModalComponent from 'client/components/groups/membersModal.vue';
describe('Members Modal Component', () => {
describe('Party Sort', () => {
let CTor;
let vm;
beforeEach(() => {
CTor = Vue.extend(MembersModalComponent);
vm = new CTor().$mount();
});
afterEach(() => {
vm.$destroy();
});
it('should have an empty object as sort-option at start', () => {
const defaultData = vm.data();
expect(defaultData.sortOption).to.eq({});
});
it('should accept sort-option object', () => {
const sortOption = vm.data().sortOption[0];
vm.sort(sortOption);
Vue.nextTick(() => {
expect(vm.data().sortOption).to.eq(sortOption);
});
});
});
});
+27 -7
View File
@@ -644,7 +644,27 @@ describe('shouldDo', () => {
day = moment();
dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
dailyTask.everyX = 3;
let threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate();
const threeWeeksFromToday = day.add(6, 'weeks').day(day.day()).toDate();
expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
});
it('activates Daily on every (x) week on weekday across a year', () => {
dailyTask.repeat = {
su: false,
s: false,
f: false,
th: false,
w: false,
t: false,
m: false,
};
day = moment('2017-11-19');
dailyTask.startDate = day.toDate();
dailyTask.repeat[DAY_MAPPING[day.day()]] = true;
dailyTask.everyX = 3;
const threeWeeksFromToday = moment('2018-01-21');
expect(shouldDo(threeWeeksFromToday, dailyTask, options)).to.equal(true);
});
@@ -970,7 +990,7 @@ describe('shouldDo', () => {
m: false,
};
let today = moment('2017-01-26');
let today = moment('2017-01-26:00:00.000-00:00');
let week = today.monthWeek();
let dayOfWeek = today.day();
dailyTask.startDate = today.toDate();
@@ -979,7 +999,7 @@ describe('shouldDo', () => {
dailyTask.everyX = 2;
dailyTask.frequency = 'monthly';
day = moment('2017-03-24');
day = moment('2017-03-24:00:00.000-00:00');
expect(shouldDo(day, dailyTask, options)).to.equal(false);
});
@@ -995,7 +1015,7 @@ describe('shouldDo', () => {
m: false,
};
let today = moment('2017-01-27');
let today = moment('2017-01-27:00:00.000-00:00');
let week = today.monthWeek();
let dayOfWeek = today.day();
dailyTask.startDate = today.toDate();
@@ -1004,7 +1024,7 @@ describe('shouldDo', () => {
dailyTask.everyX = 2;
dailyTask.frequency = 'monthly';
day = moment('2017-03-24');
day = moment('2017-03-24:00:00.000-00:00');
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
@@ -1020,7 +1040,7 @@ describe('shouldDo', () => {
m: false,
};
let today = moment('2017-01-27');
let today = moment('2017-01-27:00:00.000-00:00');
let week = today.monthWeek();
let dayOfWeek = today.day();
dailyTask.startDate = today.toDate();
@@ -1029,7 +1049,7 @@ describe('shouldDo', () => {
dailyTask.everyX = 2;
dailyTask.frequency = 'monthly';
day = moment('2017-03-24');
day = moment('2017-03-24:00:00.000-00:00');
expect(shouldDo(day, dailyTask, options)).to.equal(true);
});
Regular → Executable
View File
+14 -6
View File
@@ -3,6 +3,14 @@ const path = require('path');
const staticAssetsDirectory = './website/static/.'; // The folder where static files (not processed) live
const prodEnv = require('./prod.env');
const devEnv = require('./dev.env');
const nconf = require('nconf');
const setupNconf = require('../../website/server/libs/setupNconf');
let configFile = path.join(path.resolve(__dirname, '../../config.json'));
setupNconf(configFile);
const DEV_BASE_URL = nconf.get('BASE_URL');
module.exports = {
build: {
@@ -33,25 +41,25 @@ module.exports = {
assetsPublicPath: '/',
staticAssetsDirectory,
proxyTable: {
// proxy all requests starting with /api/v3 to localhost:3000
// proxy all requests starting with /api/v3 to IP:PORT as specified in the top-level config
'/api/v3': {
target: 'http://localhost:3000',
target: DEV_BASE_URL,
changeOrigin: true,
},
'/stripe': {
target: 'http://localhost:3000',
target: DEV_BASE_URL,
changeOrigin: true,
},
'/amazon': {
target: 'http://localhost:3000',
target: DEV_BASE_URL,
changeOrigin: true,
},
'/paypal': {
target: 'http://localhost:3000',
target: DEV_BASE_URL,
changeOrigin: true,
},
'/logout': {
target: 'http://localhost:3000',
target: DEV_BASE_URL,
changeOrigin: true,
},
},
+2 -8
View File
@@ -1,17 +1,11 @@
const nconf = require('nconf');
const { join, resolve } = require('path');
const setupNconf = require('../../website/server/libs/setupNconf');
const PATH_TO_CONFIG = join(resolve(__dirname, '../../config.json'));
let configFile = PATH_TO_CONFIG;
nconf
.argv()
.env()
.file('user', configFile);
nconf.set('IS_PROD', nconf.get('NODE_ENV') === 'production');
nconf.set('IS_DEV', nconf.get('NODE_ENV') === 'development');
nconf.set('IS_TEST', nconf.get('NODE_ENV') === 'test');
setupNconf(configFile);
// @TODO: Check if we can import from client. Items like admin emails can be imported
// and that should be prefered
+14 -12
View File
@@ -1,5 +1,6 @@
<template lang="pug">
#app(:class='{"casting-spell": castingSpell}')
amazon-payments-modal
snackbars
router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else)
@@ -82,6 +83,7 @@ import BuyModal from './components/shops/buyModal.vue';
import SelectMembersModal from 'client/components/selectMembersModal.vue';
import notifications from 'client/mixins/notifications';
import { setup as setupPayments } from 'client/libs/payments';
import amazonPaymentsModal from 'client/components/payments/amazonModal';
export default {
mixins: [notifications],
@@ -94,6 +96,7 @@ export default {
snackbars,
BuyModal,
SelectMembersModal,
amazonPaymentsModal,
},
data () {
return {
@@ -296,7 +299,6 @@ export default {
const modalId = bvEvent.target.id;
let modalStackLength = this.$store.state.modalStack.length;
let modalOnTop = this.$store.state.modalStack[modalStackLength - 1];
let modalSecondToTop = this.$store.state.modalStack[modalStackLength - 2];
// Don't remove modal if hid was called from main app
// @TODO: I'd reather use this, but I don't know how to pass data to hidden event
@@ -308,13 +310,15 @@ export default {
// Recalculate and show the last modal if there is one
modalStackLength = this.$store.state.modalStack.length;
modalOnTop = this.$store.state.modalStack[modalStackLength - 1];
let modalOnTop = this.$store.state.modalStack[modalStackLength - 1];
if (modalOnTop) this.$root.$emit('bv::show::modal', modalOnTop, {fromRoot: true});
});
},
methods: {
resetItemToBuy ($event) {
if (!$event) {
// @TODO: Do we need this? I think selecting a new item
// overwrites. @negue might know
if (!$event && this.selectedItemToBuy.purchaseType !== 'card') {
this.selectedItemToBuy = null;
}
},
@@ -332,21 +336,19 @@ export default {
},
customPurchase (item) {
if (item.purchaseType === 'card') {
if (this.user.party._id) {
this.selectedSpellToBuy = item;
this.selectedSpellToBuy = item;
this.$root.$emit('bv::hide::modal', 'buy-modal');
this.$root.$emit('bv::show::modal', 'select-member-modal');
} else {
this.error(this.$t('errorNotInParty'));
}
this.$root.$emit('bv::hide::modal', 'buy-modal');
this.$root.$emit('bv::show::modal', 'select-member-modal');
}
},
async memberSelected (member) {
this.$store.dispatch('user:castSpell', {key: this.selectedSpellToBuy.key, targetId: member.id});
this.selectedSpellToBuy = null;
this.$store.dispatch('party:getMembers', {forceLoad: true});
if (this.user.party._id) {
this.$store.dispatch('party:getMembers', {forceLoad: true});
}
this.$root.$emit('bv::hide::modal', 'select-member-modal');
},
@@ -382,4 +384,4 @@ export default {
<style src="assets/css/sprites/spritesmith-main-18.css"></style>
<style src="assets/css/sprites/spritesmith-main-19.css"></style>
<style src="assets/css/sprites/spritesmith-main-20.css"></style>
<style src="assets/css/sprites.css"></style>
<style src="assets/css/sprites.css"></style>
@@ -1,24 +1,24 @@
.promo_mystery_201711 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -783px 0px;
background-position: -499px -202px;
width: 141px;
height: 294px;
}
.promo_potions_thunderstorm {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -499px 0px;
background-position: -842px 0px;
width: 141px;
height: 441px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -783px -295px;
background-position: -641px -202px;
width: 114px;
height: 87px;
}
.promo_turkey_day_2017 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -641px 0px;
background-position: 0px -515px;
width: 141px;
height: 441px;
}
@@ -34,3 +34,9 @@
width: 302px;
height: 264px;
}
.scene_money {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -499px 0px;
width: 342px;
height: 201px;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

After

Width:  |  Height:  |  Size: 334 KiB

+21 -21
View File
@@ -139,27 +139,6 @@
}
&-better {
background: $blue-50;
&-color {
color: darken($blue-50, 12%);
}
&-control-habit {
background: darken($blue-50, 12%);
}
&-control-daily-todo {
background: $blue-500;
color: $blue-50;
}
&-modal-input {
color: $blue-500 !important;
}
}
&-best {
background: $teal-50;
&-color {
@@ -180,6 +159,27 @@
}
}
&-best {
background: $blue-50;
&-color {
color: darken($blue-50, 12%);
}
&-control-habit {
background: darken($blue-50, 12%);
}
&-control-daily-todo {
background: $blue-500;
color: $blue-50;
}
&-modal-input {
color: $blue-500 !important;
}
}
&-reward {
background: #FFF5E5
}
+4 -4
View File
@@ -2,8 +2,8 @@
// possible values are: normal, fall, habitoween, thanksgiving
// more to be added on future seasons
$npc_market_flavor: 'thanksgiving';
$npc_quests_flavor: 'thanksgiving';
$npc_seasonal_flavor: 'thanksgiving';
$npc_market_flavor: 'normal';
$npc_quests_flavor: 'normal';
$npc_seasonal_flavor: 'normal';
$npc_timetravelers_flavor: 'normal';
$npc_tavern_flavor: 'thanksgiving';
$npc_tavern_flavor: 'normal';
@@ -154,6 +154,7 @@ export default {
this.$root.$emit('bv::hide::modal', 'choose-class');
},
clickSelectClass (heroClass) {
if (this.user.flags.classSelected && !confirm(this.$t('changeClassConfirmCost'))) return;
this.$store.dispatch('user:changeClass', {query: {class: heroClass}});
},
clickDisableClasses () {
+8 -4
View File
@@ -1,6 +1,6 @@
<template lang="pug">
.row
buy-gems-modal(v-if="isUserLoaded")
buy-gems-modal(v-if='user')
modify-inventory(v-if="isUserLoaded")
footer.col-12(:class="{expanded: isExpandedFooter}")
.row(v-if="isExpandedFooter")
@@ -71,9 +71,13 @@
.row
.col-10 {{ $t('donateText3') }}
.col-2
button.btn.btn-donate(@click="donate()")
button.btn.btn-contribute(@click="donate()", v-if="user")
.svg-icon.heart(v-html="icons.heart")
.text {{ $t('companyDonate') }}
.btn.btn-contribute(v-else)
a(href='http://habitica.wikia.com/wiki/Contributing_to_Habitica', target='_blank')
.svg-icon.heart(v-html="icons.heart")
.text {{ $t('companyContribute') }}
.row
.col-12
hr
@@ -211,7 +215,7 @@
padding: 2em;
}
.btn-donate {
.btn-contribute {
background: #c3c0c7;
box-shadow: none;
border-radius: 4px;
@@ -369,7 +373,7 @@ export default {
eventAction: 'click',
eventLabel: 'Gems > Donate',
});
this.$root.$emit('bv::show::modal', 'buy-gems');
this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true});
},
},
};
+1 -1
View File
@@ -24,7 +24,7 @@
span.head_0
span(:class="getGearClass('back_collar')")
span(:class="getGearClass('body')")
template(v-for="type in ['base', 'bangs', 'mustache', 'beard']")
template(v-for="type in ['bangs', 'base', 'mustache', 'beard']")
span(:class="'hair_' + type + '_' + member.preferences.hair[type] + '_' + member.preferences.hair.color")
span(:class="getGearClass('eyewear')")
span(:class="getGearClass('head')")
@@ -1,6 +1,7 @@
<template lang="pug">
.row
challenge-modal(:cloning='cloning' v-on:updatedChallenge='updatedChallenge')
leave-challenge-modal(:challengeId='challenge._id')
close-challenge-modal(:members='members', :challengeId='challenge._id')
challenge-member-progress-modal(:memberId='progressMemberId', :challengeId='challenge._id')
@@ -33,7 +34,8 @@
span.view-progress
strong {{ $t('viewProgressOf') }}
b-dropdown.create-dropdown(text="Select a Participant")
b-dropdown-item(v-for="member in members", :key="member._id", @click="openMemberProgressModal(member._id)")
input.form-control(type='text', v-model='searchTerm')
b-dropdown-item(v-for="member in memberResults", :key="member._id", @click="openMemberProgressModal(member._id)")
| {{ member.profile.name }}
span(v-if='isLeader || isAdmin')
b-dropdown.create-dropdown(:text="$t('addTaskToChallenge')", :variant="'success'")
@@ -189,6 +191,8 @@ import TaskModal from '../tasks/taskModal';
import markdownDirective from 'client/directives/markdown';
import challengeModal from './challengeModal';
import challengeMemberProgressModal from './challengeMemberProgressModal';
import challengeMemberSearchMixin from 'client/mixins/challengeMemberSearch';
import leaveChallengeModal from './leaveChallengeModal';
import taskDefaults from 'common/script/libs/taskDefaults';
@@ -198,11 +202,13 @@ import calendarIcon from 'assets/svg/calendar.svg';
export default {
props: ['challengeId'],
mixins: [challengeMemberSearchMixin],
directives: {
markdown: markdownDirective,
},
components: {
closeChallengeModal,
leaveChallengeModal,
challengeModal,
challengeMemberProgressModal,
TaskColumn: Column,
@@ -231,6 +237,8 @@ export default {
workingTask: {},
taskFormPurpose: 'create',
progressMemberId: '',
searchTerm: '',
memberResults: [],
};
},
computed: {
@@ -365,19 +373,7 @@ export default {
await this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true});
},
async leaveChallenge () {
let keepChallenge = confirm('Do you want to keep challenge tasks?');
let keep = 'keep-all';
if (!keepChallenge) keep = 'remove-all';
let index = findIndex(this.user.challenges, (challengeId) => {
return challengeId === this.searchId;
});
this.user.challenges.splice(index, 1);
await this.$store.dispatch('challenges:leaveChallenge', {
challengeId: this.searchId,
keep,
});
await this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true});
this.$root.$emit('bv::show::modal', 'leave-challenge-modal');
},
closeChallenge () {
this.$root.$emit('bv::show::modal', 'close-challenge-modal');
@@ -57,8 +57,9 @@
You do not have enough gems to create a Tavern challenge
// @TODO if buy gems button is added, add analytics tracking to it
// see https://github.com/HabitRPG/habitica/blob/develop/website/views/options/social/challenges.jade#L134
button.btn.btn-primary(v-once, v-if='creating', @click='createChallenge()') {{$t('createChallengeCloneTasks')}}
button.btn.btn-primary(v-once, v-if='!creating', @click='updateChallenge()') {{$t('updateChallenge')}}
button.btn.btn-primary(v-once, v-if='creating && !cloning', @click='createChallenge()') {{$t('createChallengeAddTasks')}}
button.btn.btn-primary(v-once, v-if='cloning', @click='createChallenge()') {{$t('createChallengeCloneTasks')}}
button.btn.btn-primary(v-once, v-if='!creating && !cloning', @click='updateChallenge()') {{$t('updateChallenge')}}
.col-12.text-center
p(v-once) {{$t('challengeMinimum')}}
</template>
@@ -74,10 +74,11 @@ div
</style>
<script>
import debounce from 'lodash/debounce';
import challengeMemberSearchMixin from 'client/mixins/challengeMemberSearch';
export default {
props: ['challengeId', 'members'],
mixins: [challengeMemberSearchMixin],
data () {
return {
winner: {},
@@ -85,14 +86,6 @@ export default {
memberResults: [],
};
},
watch: {
searchTerm: debounce(function searchTerm (newSearch) {
this.searchChallengeMember(newSearch);
}, 500),
members () {
this.memberResults = this.members;
},
},
computed: {
winnerText () {
if (!this.winner.profile) return this.$t('selectMember');
@@ -100,12 +93,6 @@ export default {
},
},
methods: {
async searchChallengeMember (search) {
this.memberResults = await this.$store.dispatch('members:getChallengeMembers', {
challengeId: this.challengeId,
searchTerm: search,
});
},
selectMember (member) {
this.winner = member;
},
@@ -0,0 +1,45 @@
<template lang="pug">
b-modal#leave-challenge-modal(:title="$t('leaveChallenge')", size='sm', :hide-footer="true")
.modal-body
h2 {{ $t('confirmKeepChallengeTasks') }}
div
button.btn.btn-primary(@click='leaveChallenge("keep")') {{ $t('keepIt') }}
button.btn.btn-danger(@click='leaveChallenge("remove-all")') {{ $t('removeIt') }}
</template>
<style scoped>
.modal-body {
padding-bottom: 2em;
}
</style>
<script>
import findIndex from 'lodash/findIndex';
import { mapState } from 'client/libs/store';
import notifications from 'client/mixins/notifications';
export default {
props: ['challengeId'],
mixins: [notifications],
computed: {
...mapState({user: 'user.data'}),
},
methods: {
async leaveChallenge (keep) {
let index = findIndex(this.user.challenges, (id) => {
return id === this.challengeId;
});
this.user.challenges.splice(index, 1);
await this.$store.dispatch('challenges:leaveChallenge', {
challengeId: this.challengeId,
keep,
});
await this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true});
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'leave-challenge-modal');
},
},
};
</script>
@@ -1,5 +1,5 @@
<template lang="pug">
.col-2.standard-sidebar.hidden-xs-down
.standard-sidebar.d-none.d-sm-block
.form-group
input.form-control.search(type="text", :placeholder="$t('search')", v-model='searchTerm')
@@ -83,19 +83,19 @@ export default {
},
{
label: 'mental_health',
key: 'mental_health ',
key: 'mental_health',
},
{
label: 'getting_organized',
key: 'getting_organized ',
key: 'getting_organized',
},
{
label: 'self_improvement',
key: 'self_improvement ',
key: 'self_improvement',
},
{
label: 'spirituality',
key: 'spirituality ',
key: 'spirituality',
},
{
label: 'time_management',
@@ -24,11 +24,12 @@
.message-hidden(v-if='msg.flagCount > 1 && user.contributor.admin') Message hidden
.card-body
h3.leader(
:class='userLevelStyle(cachedProfileData[msg.uuid])'
:class='userLevelStyle(msg)',
@click="showMemberModal(msg.uuid)",
v-b-tooltip.hover.top="('contributor' in msg) ? msg.contributor.text : ''",
)
| {{msg.user}}
.svg-icon(v-html="icons[`tier${cachedProfileData[msg.uuid].contributor.level}`]", v-if='cachedProfileData[msg.uuid] && cachedProfileData[msg.uuid].contributor && cachedProfileData[msg.uuid].contributor.level')
.svg-icon(v-html="icons[`tier${msg.contributor.level}`]", v-if='msg.contributor && msg.contributor.level')
p.time {{msg.timestamp | timeAgo}}
.text(v-markdown='msg.text')
hr
@@ -41,7 +42,7 @@
.svg-icon(v-html="icons.copy")
| {{$t('copyAsTodo')}}
// @TODO make copyAsTodo work in the inbox
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted', @click='report(msg)')
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
.svg-icon(v-html="icons.report")
| {{$t('report')}}
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
@@ -61,11 +62,12 @@
.message-hidden(v-if='msg.flagCount > 1 && user.contributor.admin') Message hidden
.card-body
h3.leader(
:class='userLevelStyle(cachedProfileData[msg.uuid])',
:class='userLevelStyle(msg)',
@click="showMemberModal(msg.uuid)",
v-b-tooltip.hover.top="('contributor' in msg) ? msg.contributor.text : ''",
)
| {{msg.user}}
.svg-icon(v-html="icons[`tier${cachedProfileData[msg.uuid].contributor.level}`]", v-if='cachedProfileData[msg.uuid] && cachedProfileData[msg.uuid].contributor && cachedProfileData[msg.uuid].contributor.level')
.svg-icon(v-html="icons[`tier${msg.contributor.level}`]", v-if='msg.contributor && msg.contributor.level')
p.time {{msg.timestamp | timeAgo}}
.text(v-markdown='msg.text')
hr
@@ -168,6 +170,7 @@
h3 { // this is the user name
cursor: pointer;
display: inline-block;
.svg-icon {
width: 10px;
@@ -478,10 +481,10 @@ export default {
// Open the modal only if the data is available
if (profile && !profile.rejected) {
// @TODO move to action or anyway move from here because it's super duplicate
this.$store.state.profileUser = profile;
this.$store.state.profileOptions.startingPage = 'profile';
this.$root.$emit('bv::show::modal', 'profile');
this.$root.$emit('habitica:show-profile', {
user: profile,
startingPage: 'profile',
});
}
},
},
+19 -16
View File
@@ -177,6 +177,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
span 5
button.btn.btn-secondary.purchase-all(@click='unlock(`hair.beard.${baseHair5Keys.join(",hair.beard.")}`)') {{ $t('purchaseAll') }}
.col-12.customize-options(v-if='editing')
.head_0.option(@click='set({"preferences.hair.mustache": 0})', :class="[{ active: user.preferences.hair.mustache === 0 }, 'hair_base_0_' + user.preferences.hair.color]")
.option(v-for='option in baseHair6',
:class='{active: option.active, locked: option.locked}')
.base.sprite.customize-option(:class="`hair_mustache_${option.key}_${user.preferences.hair.color}`", @click='option.click')
@@ -538,12 +539,6 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
padding-bottom: 2em;
}
.option.locked {
border-radius: 2px;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
}
.option.hide {
display: none !important;
}
@@ -554,8 +549,17 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
padding: .5em;
height: 90px;
width: 90px;
margin-bottom: .5em;
margin-right: .5em;
margin: 1em .5em .5em 0;
border: 4px solid $gray-700;
border-radius: 4px;
&.locked {
border: none;
border-radius: 2px;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
margin-top: 0;
}
.sprite.customize-option {
margin: 0 auto;
@@ -587,9 +591,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
}
.option.active {
border: 4px solid $purple-200;
border-radius: 4px;
margin-top: 1em;
border-color: $purple-200;
}
.option:hover {
@@ -1023,7 +1025,8 @@ export default {
option.key = key;
option.active = this.user.preferences.costume ? this.user.items.gear.costume.eyewear === newKey : this.user.items.gear.equipped.eyewear === newKey;
option.click = () => {
return this.equip(newKey);
let type = this.user.preferences.costume ? 'costume' : 'equipped';
return this.equip(newKey, type);
};
return option;
});
@@ -1061,7 +1064,8 @@ export default {
option.active = this.user.preferences.costume ? this.user.items.gear.costume.headAccessory === newKey : this.user.items.gear.equipped.headAccessory === newKey;
option.locked = locked;
option.click = () => {
return locked ? this.purchase('gear', newKey) : this.equip(newKey);
let type = this.user.preferences.costume ? 'costume' : 'equipped';
return locked ? this.purchase('gear', newKey) : this.equip(newKey, type);
};
return option;
});
@@ -1306,9 +1310,8 @@ export default {
set (settings) {
this.$store.dispatch('user:set', settings);
},
equip (key) {
this.$store.dispatch('common:equip', {key, type: 'equipped'});
this.user.items.gear.equipped[key] = !this.user.items.gear.equipped[key];
equip (key, type) {
this.$store.dispatch('common:equip', {key, type});
},
async done () {
this.loading = true;
@@ -137,6 +137,7 @@ export default {
// Reset the page when filters are updated
this.lastPageLoaded = 0;
this.hasLoadedAllGuilds = false;
this.queryFilters.page = this.lastPageLoaded;
this.queryFilters.categories = eventData.categories.join(',');
@@ -173,10 +174,11 @@ export default {
},
async fetchGuilds () {
// We have the data cached
if (this.lastPageLoaded === 0 && this.guilds.length > 0) return;
if (this.lastPageLoaded === 0 && this.guilds.length > 0) {
this.lastPageLoaded += 1;
}
this.loading = true;
this.queryFilters.page = this.lastPageLoaded;
let guilds = await this.$store.dispatch('guilds:getPublicGuilds', this.queryFilters);
if (guilds.length === 0) this.hasLoadedAllGuilds = true;
+19 -4
View File
@@ -123,7 +123,7 @@
.col-6
span.float-left
| {{parseFloat(group.quest.progress.hp).toFixed(2)}} / {{parseFloat(questData.boss.hp).toFixed(2)}}
.col-6
.col-6(v-if='userIsOnQuest')
// @TODO: Why do we not sync quset progress on the group doc? Each user could have different progress
span.float-right {{parseFloat(user.party.quest.progress.up).toFixed(1) || 0}} pending damage
.row.rage-bar-row(v-if='questData.boss.rage')
@@ -184,6 +184,16 @@
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
@media (min-width: 1300px) {
.standard-page {
max-width: 80%;
}
.sidebar {
max-width: 430px !important;
}
}
h1 {
color: $purple-200;
}
@@ -559,6 +569,10 @@ export default {
},
computed: {
...mapState({user: 'user.data'}),
userIsOnQuest () {
if (!this.group.quest || !this.group.quest.members) return false;
return Boolean(this.group.quest.members[this.user._id]);
},
acceptedCount () {
let count = 0;
@@ -869,9 +883,10 @@ export default {
},
async showMemberProfile (leader) {
let heroDetails = await this.$store.dispatch('members:fetchMember', { memberId: leader._id });
this.$store.state.profileUser = heroDetails.data.data;
this.$store.state.profileOptions.startingPage = 'profile';
this.$root.$emit('bv::show::modal', 'profile');
this.$root.$emit('habitica:show-profile', {
user: heroDetails.data.data,
startingPage: 'profile',
});
},
async questAbort () {
if (!confirm(this.$t('sureAbort'))) return;
@@ -11,7 +11,7 @@
select.form-control(v-model="workingGroup.newLeader")
option(v-for='potentialLeader in potentialLeaders', :value="potentialLeader._id") {{ potentialLeader.name }}
.form-group(v-if='!this.workingGroup.id')
.form-group
label
strong(v-once) {{$t('privacySettings')}} *
br
@@ -35,7 +35,7 @@
// @TODO discuss the impact of this with moderators before implementing
br
label.custom-control.custom-checkbox(v-if='!isParty')
label.custom-control.custom-checkbox(v-if='!isParty && !this.workingGroup.id')
input.custom-control-input(type="checkbox", v-model="workingGroup.privateGuild")
span.custom-control-indicator
span.custom-control-description(v-once) {{ $t('privateGuild') }}
@@ -345,7 +345,13 @@ export default {
if (editingGroup.summary) this.workingGroup.summary = editingGroup.summary;
if (editingGroup.description) this.workingGroup.description = editingGroup.description;
if (editingGroup._id) this.workingGroup.id = editingGroup._id;
if (editingGroup.leader._id) this.workingGroup.newLeader = editingGroup.leader._id;
this.workingGroup.onlyLeaderCreatesChallenges = editingGroup.leaderOnly.challenges;
if (editingGroup.leader._id) {
this.workingGroup.newLeader = editingGroup.leader._id;
this.workingGroup.currentLeaderId = editingGroup.leader._id;
}
if (editingGroup._id) this.getMembers();
},
},
@@ -407,11 +413,9 @@ export default {
this.workingGroup.privacy = 'public';
}
if (!this.workingGroup.onlyLeaderCreatesChallenges) {
this.workingGroup.leaderOnly = {
challenges: true,
};
}
this.workingGroup.leaderOnly = {
challenges: this.workingGroup.onlyLeaderCreatesChallenges,
};
let categoryKeys = this.workingGroup.categories;
let serverCategories = [];
@@ -424,14 +428,19 @@ export default {
});
this.workingGroup.categories = serverCategories;
let groupData = Object.assign({}, this.workingGroup);
if (groupData.newLeader === this.workingGroup.currentLeaderId) {
groupData.leader = this.workingGroup.currentLeaderId;
}
let newgroup;
if (this.workingGroup.id) {
await this.$store.dispatch('guilds:update', {group: this.workingGroup});
if (groupData.id) {
await this.$store.dispatch('guilds:update', {group: groupData});
this.$root.$emit('updatedGroup', this.workingGroup);
// @TODO: this doesn't work because of the async resource
// if (updatedGroup.type === 'party') this.$store.state.party = {data: updatedGroup};
} else {
newgroup = await this.$store.dispatch('guilds:create', {group: this.workingGroup});
newgroup = await this.$store.dispatch('guilds:create', {group: groupData});
this.$store.state.user.data.balance -= 1;
}
@@ -1,7 +1,6 @@
<template lang="pug">
// @TODO: Move to group plans folder
div
amazon-payments-modal(:amazon-payments-prop='amazonPayments')
div
.header
h1.text-center Need more for your Group?
@@ -78,7 +77,7 @@ div
div Each Additional
div Member
b-modal#group-plan-modal(title="Empty", size='md', hide-footer=true)
b-modal#group-plan-modal(title="Select Payment", size='md', hide-footer=true)
.col-12(v-if='activePage === PAGES.CREATE_GROUP')
.form-group
label.control-label(for='new-group-name') Name
@@ -200,6 +199,9 @@ div
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
padding: 2em;
text-align: center;
display: inline-block !important;
vertical-align: bottom;
margin-right: 1em;
img {
margin: 0 auto;
@@ -321,7 +323,6 @@ div
<script>
import paymentsMixin from '../../mixins/payments';
import amazonPaymentsModal from '../payments/amazonModal';
import { mapState } from 'client/libs/store';
import group from 'assets/svg/group.svg';
import amazonpay from 'assets/svg/amazonpay.svg';
@@ -329,9 +330,6 @@ import positiveIcon from 'assets/svg/positive.svg';
export default {
mixins: [paymentsMixin],
components: {
amazonPaymentsModal,
},
data () {
return {
amazonPayments: {},
@@ -405,6 +403,7 @@ export default {
if (this.paymentMethod === this.PAYMENTS.STRIPE) {
this.showStripe(paymentData);
} else if (this.paymentMethod === this.PAYMENTS.AMAZON) {
paymentData.type = 'subscription';
this.amazonPaymentsInit(paymentData);
}
},
@@ -16,7 +16,7 @@ div
.col-5.offset-1
span.dropdown-label {{ $t('sortBy') }}
b-dropdown(:text="$t('sort')", right=true)
b-dropdown-item(v-for='sortOption in sortOptions', @click='sort(sortOption.value)', :key='sortOption.value') {{sortOption.text}}
b-dropdown-item(v-for='sortOption in sortOptions', @click='sort(sortOption)', :key='sortOption.value') {{sortOption.text}}
.row(v-if='invites.length > 0')
.col-6.offset-3.nav
.nav-item(@click='viewMembers()', :class="{active: selectedPage === 'members'}") {{ $t('members') }}
@@ -36,7 +36,7 @@ div
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.messageIcon")
span.text {{$t('sendMessage')}}
b-dropdown-item(@click='sort(option.value)', v-if='isLeader')
b-dropdown-item(@click='promoteToLeader(member._id)', v-if='isLeader')
span.dropdown-icon-item
.svg-icon.inline(v-html="icons.starIcon")
span.text {{$t('promoteToLeader')}}
@@ -172,7 +172,9 @@ div
</style>
<script>
import sortBy from 'lodash/sortBy';
// import sortBy from "lodash/sortBy";
import orderBy from 'lodash/orderBy';
import isEmpty from 'lodash/isEmpty';
import { mapState } from 'client/libs/store';
import removeMemberModal from 'client/components/members/removeMemberModal';
@@ -190,27 +192,83 @@ export default {
},
data () {
return {
sortOption: '',
sortOption: {},
selectedPage: 'members',
members: [],
invites: [],
memberToRemove: {},
sortOptions: [
{
value: 'level',
text: this.$t('tier'),
},
{
value: 'name',
text: this.$t('name'),
},
{
value: 'lvl',
text: this.$t('level'),
},
{
value: 'class',
text: this.$t('class'),
order: 'asc',
param: 'stats.class',
},
{
value: 'background',
text: this.$t('background'),
order: 'asc',
param: 'preferences.background',
},
{
value: 'date-joined-asc',
text: this.$t('sortDateJoinedAsc'),
order: 'asc',
param: 'auth.timestamps.created',
},
{
value: 'date-joined-desc',
text: this.$t('sortDateJoinedDesc'),
order: 'desc',
param: 'auth.timestamps.created',
},
{
value: 'login-asc',
text: this.$t('sortLoginAsc'),
order: 'asc',
param: 'auth.timestamps.loggedin',
},
{
value: 'login-desc',
text: this.$t('sortLoginDesc'),
order: 'desc',
param: 'auth.timestamps.loggedin',
},
{
value: 'level-asc',
text: this.$t('sortLevelAsc'),
order: 'asc',
param: 'stats.lvl',
},
{
value: 'level-desc',
text: this.$t('sortLevelDesc'),
order: 'desc',
param: 'stats.lvl',
},
{
value: 'name-asc',
text: this.$t('sortNameAsc'),
order: 'asc',
param: 'profile.name',
},
{
value: 'name-desc',
text: this.$t('sortNameDesc'),
order: 'desc',
param: 'profile.name',
},
{
value: 'tier-asc',
text: this.$t('sortTierAsc'),
order: 'asc',
param: 'contributor.level',
},
{
value: 'tier-desc',
text: this.$t('sortTierDesc'),
order: 'desc',
param: 'contributor.level',
},
],
searchTerm: '',
@@ -249,24 +307,18 @@ 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
);
});
}
if (!this.sortOption) return sortedMembers;
sortedMembers = sortBy(this.members, [(member) => {
if (this.sortOption === 'tier') {
if (!member.contributor) return;
return member.contributor.level;
} else if (this.sortOption === 'name') {
return member.profile.name;
} else if (this.sortOption === 'lvl') {
return member.stats.lvl;
} else if (this.sortOption === 'class') {
return member.stats.class;
}
}]);
if (!isEmpty(this.sortOption)) {
// Use the memberlist filtered by searchTerm
sortedMembers = orderBy(sortedMembers, [this.sortOption.param], [this.sortOption.order]);
}
return sortedMembers;
},
@@ -388,6 +440,12 @@ export default {
});
this.viewMembers();
},
async promoteToLeader (memberId) {
let groupData = Object.assign({}, this.group);
groupData.leader = memberId;
await this.$store.dispatch('guilds:update', {group: groupData});
this.$root.$emit('updatedGroup', groupData);
},
},
};
</script>
@@ -17,7 +17,7 @@ router-link.card-link(:to="{ name: 'guild', params: { groupId: guild._id } }")
p.summary(v-if='guild.summary') {{guild.summary.substr(0, MAX_SUMMARY_SIZE_FOR_GUILDS)}}
p.summary(v-else) {{ guild.name }}
.col-md-2.cta-container
button.btn.btn-danger(v-if='isMember && displayLeave' @click='leave()', v-once) {{ $t('leave') }}
button.btn.btn-danger(v-if='isMember && displayLeave' @click.prevent='leave()', v-once) {{ $t('leave') }}
button.btn.btn-success(v-if='!isMember' @click='join()', v-once) {{ $t('join') }}
div.item-with-icon.gem-bank(v-if='displayGemBank')
.svg-icon.gem(v-html="icons.gem")
@@ -175,7 +175,7 @@ export default {
},
async leave () {
// @TODO: ask about challenges when we add challenges
await this.$store.dispatch('guilds:leave', {guildId: this.guild._id, type: 'myGuilds'});
await this.$store.dispatch('guilds:leave', {groupId: this.guild._id, type: 'myGuilds'});
},
async reject (invitationToReject) {
// @TODO: This needs to be in the notifications where users will now accept invites
@@ -15,7 +15,7 @@
questDialogContent(:item="questData")
div.text-center.actions(v-if='canEditQuest')
div
button.btn.btn-secondary(v-once, @click="questForceStart()") {{ $t('begin') }}
button.btn.btn-secondary(v-once, @click="questConfirm()") {{ $t('begin') }}
// @TODO don't allow the party leader to start the quest until the leader has accepted or rejected the invitation (users get confused and think "begin" means "join quest")
div
.cancel(v-once, @click="questCancel()") {{ $t('cancel') }}
@@ -52,11 +52,10 @@
height: 460px;
width: 320px;
top: 2.5em;
left: -22em;
left: -22.8em;
z-index: -1;
padding: 2em;
overflow: scroll;
overflow-y: auto;
h3 {
color: $white;
}
@@ -180,7 +179,8 @@ export default {
return quests.quests[this.group.quest.key];
},
members () {
return this.partyMembers.map(member => {
let partyMembers = this.partyMembers || [];
return partyMembers.map(member => {
return {
name: member.profile.name,
accepted: this.group.quest.members[member._id],
@@ -195,6 +195,14 @@ export default {
},
},
methods: {
async questConfirm () {
let count = 0;
for (let uuid in this.group.quest.members) {
if (this.group.quest.members[uuid]) count += 1;
}
if (!confirm(this.$t('questConfirm', { questmembers: count, totalmembers: this.group.memberCount}))) return;
this.questForceStart();
},
async questForceStart () {
let quest = await this.$store.dispatch('quests:sendAction', {groupId: this.group._id, action: 'quests/force-start'});
this.group.quest = quest;
+1 -1
View File
@@ -1,5 +1,5 @@
<template lang="pug">
.standard-sidebar.hidden-xs-down
.standard-sidebar.d-none.d-sm-block
.form-group
input.form-control.search(type="text", :placeholder="$t('search')", v-model='searchTerm')
+4 -3
View File
@@ -199,9 +199,10 @@ export default {
},
async clickMember (hero) {
let heroDetails = await this.$store.dispatch('members:fetchMember', { memberId: hero._id });
this.$store.state.profileUser = heroDetails.data.data;
this.$store.state.profileOptions.startingPage = 'profile';
this.$root.$emit('bv::show::modal', 'profile');
this.$root.$emit('habitica:show-profile', {
user: heroDetails.data.data,
startingPage: 'profile',
});
},
userLevelStyle () {
// @TODO: implement
+1 -1
View File
@@ -46,7 +46,7 @@ div
.dropdown-menu
router-link.dropdown-item(:to="{name: 'myChallenges'}") {{ $t('myChallenges') }}
router-link.dropdown-item(:to="{name: 'findChallenges'}") {{ $t('findChallenges') }}
router-link.nav-item.dropdown(tag="li", to="/help", :class="{'active': $route.path.startsWith('/help')}", :to="{name: 'faq'}")
router-link.nav-item.dropdown(tag="li", :class="{'active': $route.path.startsWith('/help')}", :to="{name: 'faq'}")
a.nav-link(v-once) {{ $t('help') }}
.dropdown-menu
router-link.dropdown-item(:to="{name: 'faq'}") {{ $t('faq') }}
@@ -15,7 +15,7 @@ menu-dropdown.item-notifications(:right="true")
a.dropdown-item(v-if='user.purchased.plan.mysteryItems.length', @click='go("/inventory/items")')
span.glyphicon.glyphicon-gift
span {{ $t('newSubscriberItem') }}
a.dropdown-item(v-for='(party, index) in user.invitations.parties')
a.dropdown-item(v-for='(party, index) in user.invitations.parties', :key='party.id')
div
span.glyphicon.glyphicon-user
span {{ $t('invitedTo', {name: party.name}) }}
@@ -26,7 +26,7 @@ menu-dropdown.item-notifications(:right="true")
span.glyphicon.glyphicon-envelope
span {{ $t('cardReceived') }}
a.dropdown-item(@click.stop='clearCards()')
a.dropdown-item(v-for='(guild, index) in user.invitations.guilds')
a.dropdown-item(v-for='(guild, index) in user.invitations.guilds', :key='guild.id')
div
span.glyphicon.glyphicon-user
span {{ $t('invitedTo', {name: guild.name}) }}
@@ -34,10 +34,10 @@ menu-dropdown.item-notifications(:right="true")
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")')
@click='showProfile()')
span.glyphicon.glyphicon-plus-sign
span {{ $t('haveUnallocated', {points: user.stats.points}) }}
a.dropdown-item(v-for='message in userNewMessages')
a.dropdown-item(v-for='message in userNewMessages', :key='message.key')
span(@click='navigateToGroup(message.key)')
span.glyphicon.glyphicon-comment
span {{message.name}}
@@ -279,6 +279,12 @@ export default {
let quest = await this.$store.dispatch('quests:sendAction', {groupId: partyId, action: 'quests/reject'});
this.user.party.quest = quest;
},
showProfile () {
this.$root.$emit('habitica:show-profile', {
user: this.user,
startingPage: 'stats',
});
},
},
};
</script>
@@ -98,9 +98,10 @@ export default {
this.$root.$emit('bv::show::modal', 'inbox-modal');
},
showProfile (startingPage) {
this.$store.state.profileUser = this.user;
this.$store.state.profileOptions.startingPage = startingPage;
this.$root.$emit('bv::show::modal', 'profile');
this.$root.$emit('habitica:show-profile', {
user: this.user,
startingPage,
});
},
showBuyGemsModal (startingPage) {
this.$store.state.gemModalOptions.startingPage = startingPage;
@@ -1,6 +1,6 @@
<template lang="pug">
.row
.standard-sidebar
.standard-sidebar.d-none.d-sm-block
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
@@ -1,6 +1,6 @@
<template lang="pug">
.row(v-mousePosition="30", @mouseMoved="mouseMoved($event)")
.standard-sidebar
.standard-sidebar.d-none.d-sm-block
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
@@ -1,7 +1,7 @@
<template lang="pug">
// @TODO: breakdown to componentes and use some SOLID
.row.stable(v-mousePosition="30", @mouseMoved="mouseMoved($event)")
.standard-sidebar.col-3.hidden-xs-down
.standard-sidebar.d-none.d-sm-block
div
#npmMattStable.npc_matt
b-popover(
@@ -54,7 +54,7 @@
@change="updateHideMissing"
)
.standard-page.col-12.col-sm-9
.standard-page
.clearfix
h1.float-left.mb-4.page-header(v-once) {{ $t('stable') }}
+6 -5
View File
@@ -1,6 +1,6 @@
<template lang="pug">
.member-details(
:class="{ condensed, expanded, 'd-flex': isHeader, row: !isHeader, }",
:class="{ condensed, expanded, 'd-flex': isHeader, row: !isHeader, }",
@click='showMemberModal(member)'
)
div(:class="{ 'col-4': !isHeader }")
@@ -174,7 +174,7 @@
border-radius: 0px;
height: 10px;
}
}
}
</style>
<script>
@@ -245,9 +245,10 @@ export default {
methods: {
percent,
showMemberModal (member) {
this.$store.state.profileUser = member;
this.$store.state.profileOptions.startingPage = 'profile';
this.$root.$emit('bv::show::modal', 'profile');
this.$root.$emit('habitica:show-profile', {
user: member,
startingPage: 'profile',
});
},
},
computed: {
+13 -8
View File
@@ -2,7 +2,7 @@
div
yesterdaily-modal(
:yesterDailies='yesterDailies',
@hide="runYesterDailiesAction()",
@run-cron="runYesterDailiesAction()",
)
armoire-empty
new-stuff
@@ -210,6 +210,7 @@ export default {
},
watch: {
baileyShouldShow () {
if (this.user.needsCron) return;
this.$root.$emit('bv::show::modal', 'new-stuff');
},
userHp (after, before) {
@@ -246,7 +247,7 @@ export default {
// Append Bonus
if (money > 0 && Boolean(bonus)) {
if (bonus < 0.01) bonus = 0.01;
this.text(`+ ${this.coins(bonus)} ${this.$t('streakCoins')}`);
this.streak(`+ ${this.coins(bonus)}`);
delete this.user._tmp.streakBonus;
}
},
@@ -310,11 +311,9 @@ export default {
},
methods: {
checkUserAchievements () {
// List of prompts for user on changes. Sounds like we may need a refactor here, but it is clean for now
if (this.user.flags.newStuff) {
this.$root.$emit('bv::show::modal', 'new-stuff');
}
if (this.user.needsCron) return;
// List of prompts for user on changes. Sounds like we may need a refactor here, but it is clean for now
if (!this.user.flags.welcomed) {
this.$store.state.avatarEditorOptions.editingUser = false;
this.$root.$emit('bv::show::modal', 'avatar-modal');
@@ -411,6 +410,8 @@ export default {
this.$store.dispatch('tasks:fetchUserTasks', {forceLoad: true}),
]);
this.$store.state.isRunningYesterdailies = false;
if (this.levelBeforeYesterdailies > 0 && this.levelBeforeYesterdailies < this.user.stats.lvl) {
this.showLevelUpNotifications(this.user.stats.lvl);
}
@@ -422,10 +423,14 @@ export default {
this.$store.state.groupNotifications.push(notification);
},
async handleUserNotifications (after) {
if (!after || after.length === 0 || !Array.isArray(after)) return;
if (this.$store.state.isRunningYesterdailies) return;
if (this.user.flags.newStuff) {
this.$root.$emit('bv::show::modal', 'new-stuff');
}
if (!after || after.length === 0 || !Array.isArray(after)) return;
let notificationsToRead = [];
let scoreTaskNotification = [];
@@ -2,8 +2,8 @@
b-modal#amazon-payment(title="Amazon", :hide-footer="true", size='md')
h2.text-center Continue with Amazon
#AmazonPayButton
#AmazonPayWallet(v-if="amazonLoggedIn", style="width: 400px; height: 228px;")
#AmazonPayRecurring(v-if="amazonLoggedIn && amazonPayments.type === 'subscription'",
#AmazonPayWallet(v-if="amazonPayments.loggedIn", style="width: 400px; height: 228px;")
#AmazonPayRecurring(v-if="amazonPayments.loggedIn && amazonPayments.type === 'subscription'",
style="width: 400px; height: 140px;")
.modal-footer
.text-center
@@ -30,40 +30,35 @@ import { mapState } from 'client/libs/store';
const AMAZON_PAYMENTS = process.env.AMAZON_PAYMENTS; // eslint-disable-line
export default {
props: ['amazonPaymentsProp'],
data () {
return {
amazonPayments: {
modal: null,
type: null,
gift: null,
loggedIn: false,
paymentSelected: false,
billingAgreementId: '',
recurringConsent: false,
orderReferenceId: null,
subscription: null,
coupon: null,
},
OffAmazonPayments: {},
isAmazonSetup: false,
amazonButtonEnabled: false,
amazonPaymentsbillingAgreementId: '',
amazonPaymentspaymentSelected: false,
amazonPaymentsrecurringConsent: 'false',
amazonLoggedIn: false,
};
},
computed: {
...mapState({user: 'user.data'}),
...mapState(['isAmazonReady']),
// @TODO: Eh, idk if we should move data props here or move these props to data. But we shouldn't have both
amazonPayments () {
let amazonPayments = {
type: 'single',
loggedIn: this.amazonLoggedIn,
};
amazonPayments = Object.assign({}, amazonPayments, this.amazonPaymentsProp);
return amazonPayments;
},
amazonPaymentsCanCheckout () {
if (this.amazonPayments.type === 'single') {
return this.amazonPaymentspaymentSelected === true;
return this.amazonPayments.paymentSelected === true;
} else if (this.amazonPayments.type === 'subscription') {
return this.amazonPaymentspaymentSelected === true &&
// Mah.. one is a boolean the other a string...
this.amazonPaymentsrecurringConsent === 'true';
} else {
return false;
return this.amazonPayments.paymentSelected && this.amazonPayments.recurringConsent;
}
return false;
},
},
mounted () {
@@ -72,6 +67,21 @@ export default {
this.$store.watch(state => state.isAmazonReady, (isAmazonReady) => {
if (isAmazonReady) return this.setupAmazon();
});
this.$root.$on('habitica::pay-with-amazon', (amazonPaymentsData) => {
if (!amazonPaymentsData) return;
let amazonPayments = {
type: 'single',
loggedIn: false,
};
this.amazonPayments = Object.assign({}, amazonPayments, amazonPaymentsData);
this.$root.$emit('bv::show::modal', 'amazon-payment');
});
},
destroyed () {
this.$root.$off('habitica::pay-with-amazon');
},
methods: {
setupAmazon () {
@@ -90,25 +100,21 @@ export default {
color: 'Gold',
size: 'small',
agreementType: 'BillingAgreement',
onSignIn: async (contract) => {
this.amazonPaymentsbillingAgreementId = contract.getAmazonBillingAgreementId();
this.amazonLoggedIn = true;
this.amazonPayments.billingAgreementId = contract.getAmazonBillingAgreementId();
this.$set(this.amazonPayments, 'loggedIn', true);
if (this.amazonPayments.type === 'subscription') {
this.amazonPaymentsinitWidgets();
this.amazonInitWidgets();
} else {
let url = '/amazon/createOrderReferenceId';
let response = await axios.post(url, {
billingAgreementId: this.amazonPaymentsbillingAgreementId,
billingAgreementId: this.amazonPayments.billingAgreementId,
});
if (response.status <= 400) {
this.amazonPayments.orderReferenceId = response.data.data.orderReferenceId;
// @TODO: Clarify the deifference of these functions by renaming
this.amazonPaymentsinitWidgets();
this.amazonInitWidgets();
return;
}
@@ -116,7 +122,6 @@ export default {
alert(response.message);
}
},
authorization: () => {
window.amazon.Login.authorize({
scope: 'payments:widget',
@@ -131,7 +136,6 @@ export default {
});
});
},
onError: this.amazonOnError,
});
},
@@ -141,38 +145,29 @@ export default {
design: {
designMode: 'responsive',
},
onPaymentSelect: () => {
this.amazonPaymentspaymentSelected = true;
},
onPaymentSelect: this.amazonOnPaymentSelect,
onError: this.amazonOnError,
};
// @TODO: Check if this is duplicated below
if (this.amazonPayments.type === 'subscription') {
walletParams.agreementType = 'BillingAgreement';
walletParams.billingAgreementId = this.amazonPaymentsbillingAgreementId;
walletParams.billingAgreementId = this.amazonPayments.billingAgreementId;
walletParams.onReady = (billingAgreement) => {
this.amazonPaymentsbillingAgreementId = billingAgreement.getAmazonBillingAgreementId();
this.amazonPayments.billingAgreementId = billingAgreement.getAmazonBillingAgreementId();
new this.OffAmazonPayments.Widgets.Consent({
sellerId: AMAZON_PAYMENTS.SELLER_ID,
amazonBillingAgreementId: this.amazonPaymentsbillingAgreementId,
amazonBillingAgreementId: this.amazonPayments.billingAgreementId,
design: {
designMode: 'responsive',
},
onReady: (consent) => {
let getConsent = consent.getConsentStatus;
this.amazonPaymentsrecurringConsent = getConsent ? getConsent() : false;
this.$set(this.amazonPayments, 'recurringConsent', getConsent ? Boolean(getConsent()) : false);
},
onConsent: (consent) => {
this.amazonPaymentsrecurringConsent = consent.getConsentStatus();
this.$set(this.amazonPayments, 'recurringConsent', Boolean(consent.getConsentStatus()));
},
onError: this.amazonOnError,
}).bind('AmazonPayRecurring');
};
@@ -191,7 +186,7 @@ export default {
let url = '/amazon/checkout';
let response = await axios.post(url, {
orderReferenceId: this.amazonPayments.orderReferenceId,
gift: this.amazonPaymentsgift,
gift: this.amazonPayments.gift,
});
if (response.status < 400) {
@@ -211,7 +206,7 @@ export default {
}
let response = await axios.post(url, {
billingAgreementId: this.amazonPaymentsbillingAgreementId,
billingAgreementId: this.amazonPayments.billingAgreementId,
subscription: this.amazonPayments.subscription,
coupon: this.amazonPayments.coupon,
groupId: this.amazonPayments.groupId,
@@ -243,67 +238,26 @@ export default {
this.reset();
}
},
amazonPaymentsinitWidgets () {
let walletParams = {
sellerId: AMAZON_PAYMENTS.SELLER_ID,
design: {
designMode: 'responsive',
},
onPaymentSelect: () => {
this.amazonPayments.paymentSelected = true;
},
onError: this.amazonOnError,
};
if (this.amazonPayments.type === 'subscription') {
walletParams.agreementType = 'BillingAgreement';
walletParams.billingAgreementId = this.amazonPayments.billingAgreementId;
walletParams.onReady = (billingAgreement) => {
this.amazonPayments.billingAgreementId = billingAgreement.getAmazonBillingAgreementId();
new this.OffAmazonPayments.Widgets.Consent({
sellerId: AMAZON_PAYMENTS.SELLER_ID,
amazonBillingAgreementId: this.amazonPayments.billingAgreementId,
design: {
designMode: 'responsive',
},
onReady: (consent) => {
let getConsent = consent.getConsentStatus;
this.amazonPayments.recurringConsent = getConsent ? getConsent() : false;
},
onConsent: (consent) => {
this.amazonPayments.recurringConsent = consent.getConsentStatus();
},
onError: this.amazonOnError,
}).bind('AmazonPayRecurring');
};
} else {
walletParams.amazonOrderReferenceId = this.amazonPayments.orderReferenceId;
}
new this.OffAmazonPayments.Widgets.Wallet(walletParams).bind('AmazonPayWallet');
amazonOnPaymentSelect () {
this.$set(this.amazonPayments, 'paymentSelected', true);
},
amazonOnError (error) {
alert(error.getErrorMessage());
this.reset();
},
reset () {
this.amazonPaymentsmodal = null;
// @TODO: Ensure we are using all of these
// some vars are set in the payments mixin. We should try to edit in one place
this.amazonPayments.modal = null;
this.amazonPayments.type = null;
this.amazonLoggedIn = false;
this.amazonPaymentsgift = null;
this.amazonPaymentsbillingAgreementId = null;
this.amazonPayments.loggedIn = false;
this.amazonPayments.gift = null;
this.amazonPayments.billingAgreementId = null;
this.amazonPayments.orderReferenceId = null;
this.amazonPaymentspaymentSelected = false;
this.amazonPaymentsrecurringConsent = false;
this.amazonPaymentssubscription = null;
this.amazonPaymentscoupon = null;
this.amazonPayments.paymentSelected = false;
this.amazonPayments.recurringConsent = false;
this.amazonPayments.subscription = null;
this.amazonPayments.coupon = null;
},
},
};
@@ -7,7 +7,6 @@
.col-md-8.align-self-center
p=text
div(v-if='user')
amazon-payments-modal(:amazon-payments-prop='amazonPayments')
b-modal(:hide-footer='true', :hide-header='true', :id='"buy-gems"', size='lg')
.container-fluid.purple-gradient
.gemfall
@@ -344,7 +343,6 @@
import markdown from 'client/directives/markdown';
import planGemLimits from 'common/script/libs/planGemLimits';
import paymentsMixin from 'client/mixins/payments';
import amazonPaymentsModal from './amazonModal';
import checkIcon from 'assets/svg/check.svg';
import creditCard from 'assets/svg/credit-card.svg';
@@ -360,7 +358,6 @@
mixins: [paymentsMixin],
components: {
planGemLimits,
amazonPaymentsModal,
},
computed: {
...mapState({user: 'user.data'}),
@@ -5,7 +5,6 @@ b-modal#send-gems(:title="title", :hide-footer="true", size='lg')
:class="gift.type === 'gems' ? 'panel-primary' : 'transparent'",
@click='gift.type = "gems"'
)
// @TODO the panel does not exists in Bootstrap 4
h3.panel-heading.clearfix
.float-right
span(v-if='gift.gems.fromBalance') {{ $t('sendGiftGemsBalance', {number: userLoggedIn.balance * 4}) }}
@@ -51,26 +50,26 @@ b-modal#send-gems(:title="title", :hide-footer="true", size='lg')
</template>
<style lang="scss">
.panel {
margin-bottom: 4px;
.panel {
margin-bottom: 4px;
&.transparent {
.panel-body {
opacity: 0.7;
}
}
.panel-heading {
margin-top: 8px;
margin-bottom: 5px;
}
&.transparent {
.panel-body {
opacity: 0.7;
padding: 8px;
border-radius: 2px;
border: 1px solid #C3C0C7;
}
}
.panel-heading {
margin-top: 8px;
margin-bottom: 5px;
}
.panel-body {
padding: 8px;
border-radius: 2px;
border: 1px solid #C3C0C7;
}
}
</style>
<script>
@@ -92,6 +92,8 @@ import removeIcon from 'assets/members/remove.svg';
import messageIcon from 'assets/members/message.svg';
import starIcon from 'assets/members/star.svg';
import { mapState } from 'client/libs/store';
export default {
props: ['group', 'hideBadge', 'item'],
components: {
@@ -129,11 +131,12 @@ export default {
};
},
computed: {
...mapState({user: 'user.data'}),
sortedMembers () {
let sortedMembers = this.members;
if (!this.sortOption) return sortedMembers;
sortedMembers = sortBy(this.members, [(member) => {
sortBy(this.members, [(member) => {
if (this.sortOption === 'tier') {
if (!member.contributor) return;
return member.contributor.level;
@@ -173,6 +176,10 @@ export default {
if (this.$store.state.memberModalOptions.viewingMembers.length > 0) {
this.members = this.$store.state.viewingMembers;
}
if (this.members.length === 0 && !this.groupId) {
this.members = [this.user];
}
},
close () {
this.$root.$emit('bv::hide::modal', 'select-member-modal');
@@ -18,7 +18,7 @@
.social-delete(v-if='!user.auth.local.email')
h4 {{ $t('deleteAccount') }}
.modal-body
p {{ $t('deleteSocialAccountText') }}
p {{ $t('deleteSocialAccountText', {magicWord: 'DELETE'}) }}
br
.row
.col-md-6
@@ -85,6 +85,10 @@ export default {
return;
}
const userChangedLevel = this.restoreValues.stats.lvl !== this.user.stats.lvl;
const userDidNotChangeExp = this.restoreValues.stats.exp === this.user.stats.exp;
if (userChangedLevel && userDidNotChangeExp) this.restoreValues.stats.exp = 0;
this.user.stats = clone(this.restoreValues.stats);
this.user.achievements.streak = clone(this.restoreValues.achievements.streak);
@@ -341,7 +341,6 @@ export default {
await axios.put(`/api/v3/user/auth/update-${attribute}`, updates);
alert(this.$t(`${attribute}Success`));
this.user[attribute] = updates[attribute];
updates = {};
},
openRestoreModal () {
this.$root.$emit('bv::show::modal', 'restore');
@@ -1,7 +1,5 @@
<template lang="pug">
.standard-page
amazon-payments-modal(:amazon-payments-prop='amazonPayments')
h1 {{ $t('subscription') }}
.row
.col-6
@@ -33,7 +31,7 @@
span.noninteractive-button.btn-danger {{ $t('canceledSubscription') }}
i.glyphicon.glyphicon-time
| {{ $t('subCanceled') }} &nbsp;
strong {{user.purchased.plan.dateTerminated | date}}
strong {{dateTerminated}}
tr(v-if='!hasCanceledSubscription'): td
h4 {{ $t('subscribed') }}
p(v-if='hasPlan && !hasGroupPlan') {{ $t('purchasedPlanId', purchasedPlanIdInfo) }}
@@ -70,7 +68,7 @@
div(v-if='hasSubscription')
.btn.btn-primary(v-if='canEditCardDetails', @click='showStripeEdit()') {{ $t('subUpdateCard') }}
.btn.btn-sm.btn-danger(v-if='canCancelSubscription', @click='cancelSubscription()') {{ $t('cancelSub') }}
.btn.btn-sm.btn-danger(v-if='canCancelSubscription && !loading', @click='cancelSubscription()') {{ $t('cancelSub') }}
small(v-if='!canCancelSubscription', v-html='getCancelSubInfo()')
.subscribe-pay(v-if='!hasSubscription || hasCanceledSubscription')
@@ -82,9 +80,8 @@
a.purchase(:href='paypalPurchaseLink', :disabled='!subscription.key', target='_blank')
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png', :alt="$t('paypal')")
.col-md-4
a.btn.btn-secondary.purchase(@click="amazonPaymentsInit({type: 'subscription', subscription:subscription.key, coupon:subscription.coupon})")
a.btn.btn-secondary.purchase(@click="payWithAmazon()")
img(src='https://payments.amazon.com/gp/cba/button', :alt="$t('amazonPayments')")
.row
.col-6
h2 {{ $t('giftSubscription') }}
@@ -115,16 +112,13 @@ import { mapState } from 'client/libs/store';
import subscriptionBlocks from '../../../common/script/content/subscriptionBlocks';
import planGemLimits from '../../../common/script/libs/planGemLimits';
import amazonPaymentsModal from '../payments/amazonModal';
import paymentsMixin from '../../mixins/payments';
export default {
mixins: [paymentsMixin],
components: {
amazonPaymentsModal,
},
data () {
return {
loading: false,
gemCostTranslation: {
gemCost: planGemLimits.convRate,
gemLimit: planGemLimits.convRate,
@@ -132,6 +126,7 @@ export default {
subscription: {
key: 'basic_earned',
},
// @TODO: Remove the need for this or move it to mixin
amazonPayments: {},
paymentMethods: {
AMAZON_PAYMENTS: 'Amazon Payments',
@@ -143,13 +138,6 @@ export default {
},
};
},
filters: {
date (value) {
if (!value) return '';
return moment(value);
// return moment(value).format(this.user.preferences.dateFormat); // @TODO make that work (`TypeError: this is undefined`)
},
},
computed: {
...mapState({user: 'user.data', credentials: 'credentials'}),
subscriptionBlocksOrdered () {
@@ -237,6 +225,10 @@ export default {
amount: this.numberOfMysticHourglasses,
};
},
dateTerminated () {
if (!this.user.preferences || !this.user.preferences.dateFormat) return this.user.purchased.plan.dateTerminated;
return moment(this.user.purchased.plan.dateTerminated).format(this.user.preferences.dateFormat.toUpperCase());
},
canCancelSubscription () {
return (
this.user.purchased.plan.paymentMethod !== this.paymentMethods.GOOGLE &&
@@ -247,6 +239,13 @@ export default {
},
},
methods: {
payWithAmazon () {
this.amazonPaymentsInit({
type: 'subscription',
subscription: this.subscription.key,
coupon: this.subscription.coupon,
});
},
async applyCoupon (coupon) {
let response = await axios.get(`/api/v3/coupons/validate/${coupon}`);
+25 -3
View File
@@ -42,10 +42,10 @@
)
.purchase-amount(:class="{'notEnough': !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}")
.how-many-to-buy(v-if='["fortify", "gear"].indexOf(item.purchaseType) === -1')
.how-many-to-buy(v-if='showAmountToBuy(item)')
strong {{ $t('howManyToBuy') }}
div(v-if='item.purchaseType !== "gear"')
.box(v-if='["fortify", "gear"].indexOf(item.purchaseType) === -1')
div(v-if='showAmountToBuy(item)')
.box
input(type='number', min='0', v-model='selectedAmountToBuy')
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
span.value(:class="getPriceClass()") {{ item.value }}
@@ -285,6 +285,11 @@
import moment from 'moment';
const hideAmountSelectionForPurchaseTypes = [
'gear', 'backgrounds', 'mystery_set', 'card',
'rebirth_orb', 'fortify', 'armoire',
];
export default {
mixins: [currencyMixin, notifications, spellsMixin, buyMixin],
components: {
@@ -363,6 +368,16 @@
this.$emit('change', $event);
},
buyItem () {
if (this.item.currency === 'gems' &&
!confirm(this.$t('purchaseFor', { cost: this.item.value }))) {
return;
}
if (this.item.currency === 'hourglasses' &&
!confirm(this.$t('purchaseForHourglasses', { cost: this.item.value }))) {
return;
}
if (this.item.cast) {
this.castStart(this.item);
} else if (this.genericPurchase) {
@@ -403,6 +418,13 @@
return 'gold';
}
},
showAmountToBuy (item) {
if (hideAmountSelectionForPurchaseTypes.includes(item.purchaseType)) {
return false;
} else {
return true;
}
},
getAvatarOverrides (item) {
switch (item.purchaseType) {
case 'gear':
@@ -1,9 +1,8 @@
<template lang="pug">
.row.market
.standard-sidebar
.standard-sidebar.d-none.d-sm-block
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
.form
h2(v-once) {{ $t('filter') }}
.form-group
@@ -15,7 +14,6 @@
input.custom-control-input(type="checkbox", v-model="viewOptions[category.identifier].selected")
span.custom-control-indicator
span.custom-control-description(v-once) {{ category.text }}
div.form-group.clearfix
h3.float-left(v-once) {{ $t('hideLocked') }}
toggle-switch.float-right.no-margin(
@@ -62,7 +60,7 @@
h1.mb-4.page-header(v-once) {{ $t('market') }}
.clearfix
.clearfix(v-if="viewOptions['equipment'].selected")
h2.float-left.mb-3
| {{ $t('equipment') }}
@@ -99,7 +97,8 @@
:itemWidth=94,
:itemMargin=24,
:type="'gear'",
:noItemsLabel="$t('noGearItemsOfClass')"
:noItemsLabel="$t('noGearItemsOfClass')",
v-if="viewOptions['equipment'].selected"
)
template(slot="item", slot-scope="ctx")
shopItem(
@@ -247,7 +246,7 @@
height: 38px; // button + margin + padding
}
.icon-48 {
width: 48px;
height: 48px;
@@ -457,6 +456,11 @@ export default {
...this.market.categories,
];
categories.push({
identifier: 'equipment',
text: this.$t('equipment'),
});
categories.push({
identifier: 'cards',
text: this.$t('cards'),
@@ -502,9 +506,11 @@ export default {
}
categories.map((category) => {
this.$set(this.viewOptions, category.identifier, {
selected: true,
});
if (!this.viewOptions[category.identifier]) {
this.$set(this.viewOptions, category.identifier, {
selected: true,
});
}
});
return categories;
@@ -1,6 +1,6 @@
<template lang="pug">
.row.quests
.standard-sidebar
.standard-sidebar.d-none.d-sm-block
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
@@ -1,6 +1,6 @@
<template lang="pug">
.row.seasonal
.standard-sidebar
.standard-sidebar.d-none.d-sm-block
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
+2 -1
View File
@@ -225,7 +225,8 @@ div
}
},
limitedString () {
return this.$t('limitedOffer', {date: moment(seasonalShopConfig.dateRange.end).format('LL')});
return this.item.owned === false ? '' :
this.$t('limitedOffer', {date: moment(seasonalShopConfig.dateRange.end).format('LL')});
},
},
methods: {
@@ -1,6 +1,6 @@
<template lang="pug">
.row.timeTravelers
.standard-sidebar(v-if="!closed")
.standard-sidebar.d-none.d-sm-block(v-if="!closed")
.form-group
input.form-control.input-search(type="text", v-model="searchText", :placeholder="$t('search')")
@@ -333,7 +333,7 @@
currency: 'hourglasses',
key: c.identifier,
class: `shop_set_mystery_${c.identifier}`,
purchaseType: 'set_mystery',
purchaseType: 'mystery_set',
};
}),
};
@@ -4,6 +4,12 @@ transition(name="fade")
.row(v-if='notification.type === "error"')
.text.col-12
div(v-html='notification.text')
.row(v-if='notification.type === "streak"')
.text.col-7.offset-1
div {{message}}
.icon.col-4
div.svg-icon(v-html="icons.gold")
div(v-html='notification.text')
.row(v-if='["hp", "gp", "xp", "mp"].indexOf(notification.type) !== -1')
.text.col-7.offset-1
div
@@ -145,6 +151,7 @@ export default {
if (this.notification.type === 'mp') localeKey += 'Mana';
if (this.notification.type === 'xp') localeKey += 'Experience';
if (this.notification.type === 'gp') localeKey += 'Gold';
if (this.notification.type === 'streak') localeKey = 'streakCoins';
return this.$t(localeKey);
// This requires eight translatable strings, but that gives the translators the most flexibility for matching gender/number and for using idioms for lost/spent/used/gained.
},
@@ -17,7 +17,7 @@
span {{ $t('enterprisePlansDescription') }}
.row.row-margin
// TODO
a.btn.btn-primary.btn-lg.btn-block(:href="'mailto:vicky@habitica.com?subject=' + $t('enterprisePlansEmailSubject')") {{ $t('enterprisePlansButton') }}
a.btn.btn-primary.btn-lg.btn-block(:href="'mailto:vicky@habitica.com?subject=' + enterprisePlansEmailSubject") {{ $t('enterprisePlansButton') }}
br
@@ -36,11 +36,16 @@
<script>
import StaticHeader from './header.vue';
import * as Analytics from 'client/libs/analytics';
export default {
components: {
StaticHeader,
},
data () {
return {
enterprisePlansEmailSubject: 'Question regarding Enterprise Plans',
};
},
methods: {
goToNewGroupPage () {
if (!this.$store.state.isUserLoggedIn) {
@@ -63,7 +68,7 @@
eventLabel: 'Contact Us (Plans)',
});
window.location.href = `mailto:vicky@habitica.com?subject=${this.$t('enterprisePlansEmailSubject')}`;
window.location.href = `mailto:vicky@habitica.com?subject=${ this.enterprisePlansEmailSubject }`;
},
},
};
+1 -1
View File
@@ -1,5 +1,5 @@
<template lang="pug">
nav.navbar.navbar-inverse.fixed-top.navbar-toggleable-sm
nav.navbar.navbar-inverse.fixed-top.navbar-expand-sm
.navbar-header
router-link.nav-item(:to='!isUserLoggedIn ? "/static/home" : "/"')
.logo.svg-icon(v-html='icons.logo')
+16 -24
View File
@@ -4,34 +4,26 @@
.align-self-center.right-margin(:class='baileyClass')
.media-body
h1.align-self-center(v-markdown='$t("newStuff")')
h2 11/17/2017 - THANKSGIVING IN HABITICA, NOVEMBER SUBSCRIBER ITEMS, AND ANDROID UPDATE
h2 11/29/2017 - LAST CHANCE FOR CARPET RIDER SET AND THUNDERSTORM POTIONS, GUILD AND USE CASE SPOTLIGHTS
hr
.media
.promo_turkey_day_2017.right-margin
.media-body.d-flex.align-self-center.flex-column
h3 Happy Thanksgiving!
p It's Thanksgiving in Habitica! On this day Habiticans celebrate by spending time with loved ones, giving thanks, and riding their glorious turkeys into the magnificent sunset. Some of the NPCs are celebrating the occasion!
h3 Turkey Pets, Mounts, and Costume!
p For the occasion, all Habiticans have received Turkey-themed items! What items? It all depends on how many Habitica Thanksgivings you've celebrated with us. Each Thanksgiving, you'll get a new and exciting Turkey pet, mount, or gear set! Check your Stable and your pinned Rewards to see what you got!
p Thank you for using Habitica - we really love you all <3
.small by Lemoness
.promo_potions_thunderstorm.right-margin
.media-body
h3 Last Chance for Carpet Rider Set
p(v-markdown='"Reminder: there are only two more days to [subscribe](/user/settings/subscription) and receive the Carpet Rider Set! Subscribing also lets you buy gems for gold. The longer your subscription, the more gems you get!"')
p Thanks so much for your support! You help keep Habitica running.
.small by Beffymaroo
h3 Last Chance for Thunderstorm Hatching Potions
p(v-markdown='"Reminder: this is the final day to [buy Thunderstorm Hatching Potions!](/shops/market) If they come back, it won\'t be until next year at the earliest, so don\'t delay!"')
.small by Balduranne
h3 Generating Interest: Guilds and Use Cases for Money Matters
p(v-markdown='"There\'s a new [Guild Spotlight on the blog](https://habitica.wordpress.com/2017/11/28/generating-interest-guilds-for-money-matters/) that highlights the Guilds that can help you as work to improve your budgeting and saving habits! Check it out now to find Habitica\'s best money management communities."')
.media
.media-body
h3 November Subscriber Items Revealed!
p(v-markdown='"The November Subscriber Items have been revealed: the [Carpet Rider Item Set](https://habitica.com/user/settings/subscription)! You have until November 30 to receive the item set when you subscribe. If you\'re already an active subscriber, reload the site and then head to Inventory > Items to claim your gear!"')
p Subscribers also receive the ability to buy gems for gold -- the longer you subscribe, the more gems you can buy per month! There are other perks as well, such as longer access to uncompressed data and a cute Jackalope pet. Best of all, subscriptions let us keep Habitica running. Thank you very much for your support -- it means a lot to us.
.small by Lemoness
.promo_mystery_201711.left-margin
h3 Android App Update
p Theres an exciting new update to our Android app!
ul
li Weve smashed some pesky bugs, including the issue with task reordering!
li You can now view and assign attribute points (or choose auto-allocation)!
li Weve added Equipment to the Market, plus the ability to change your login name and email, reset or delete your account, make changes to your profile, and access Fix Character Values.
li You can also request a password reset from the login screen!
p We hope this makes your Habitica experience even better. Be sure to download the update now for a better Habitica experience!
p If you like the improvements that weve been making to our app, please consider reviewing this new version. It really helps us out!
.small by Viirus and piyorii
p(v-markdown='"This month\'s [Use Case Spotlight](https://habitica.wordpress.com/2017/11/14/3908/) follows the same theme! It features a number of great suggestions submitted by Habiticans in the [Use Case Spotlights Guild](https://habitica.com/groups/guild/1d3a10bf-60aa-4806-a38b-82d1084a59e6). We hope it helps any of you who might be working on saving and budgeting as the holiday season approaches!"')
p Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to stay on top of Holiday Housekeeping? Well be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog at the start of next month, so post your suggestions in the Use Case Spotlight Guild now. We look forward to learning more about how you use Habitica to improve your life and get things done!
.small by Beffymaroo
.scene_money.left-margin
br
</template>
@@ -73,7 +73,7 @@ div
color: #bda8ff;
}
.social-circle, .btn-donate {
.social-circle, .btn-contribute {
background: #36205d;
color: #bda8ff;
@@ -63,15 +63,15 @@ export default {
}
if (assignedUsersLength === 1 && !this.userIsAssigned) {
return `Assigned to ${assignedUsersNames[0]}`;
return this.$t('assignedToUser', {userName: assignedUsersNames[0]});
} else if (assignedUsersLength > 1 && !this.userIsAssigned) {
return `Assigned to ${assignedUsersLength} members`;
return this.$t('assignedToMembers', {userCount: assignedUsersLength});
} else if (assignedUsersLength > 1 && this.userIsAssigned) {
return `Assigned to you and ${assignedUsersLength} members`;
return this.$t('assignedToYouAndMembers', {userCount: assignedUsersLength});
} else if (this.userIsAssigned) {
return 'You are assigned to this task';
return this.$t('youAreAssigned');
} else if (assignedUsersLength === 0) {
return 'This task is unassigned';
return this.$t('taskIsUnassigned');
}
},
approvalRequested () {
@@ -83,7 +83,7 @@ export default {
},
methods: {
async claim () {
if (!confirm('Are you sure you want to claim this task?')) return;
if (!confirm(this.$t('confirmClaim'))) return;
let taskId = this.task._id;
// If we are on the user task
@@ -98,7 +98,7 @@ export default {
this.task.group.assignedUsers.push(this.user._id);
},
async unassign () {
if (!confirm('Are you sure you want to unclaim this task?')) return;
if (!confirm(this.$t('confirmUnClaim'))) return;
let taskId = this.task._id;
// If we are on the user task
@@ -114,7 +114,7 @@ export default {
this.task.group.assignedUsers.splice(index, 1);
},
approve () {
if (!confirm('Are you sure you want to approve this task?')) return;
if (!confirm(this.$t('confirmApproval'))) return;
let userIdToApprove = this.task.group.assignedUsers[0];
this.$store.dispatch('tasks:unassignTask', {
taskId: this.task._id,
@@ -27,11 +27,11 @@ export default {
let userIsRequesting = this.task.group.approvals && this.task.group.approvals.indexOf(this.user._id) !== -1;
if (approvalsLength === 1 && !userIsRequesting) {
return `${approvals[0].userId.profile.name} requests approval`;
return this.$t('youAreRequestingApproval', {userName: approvals[0].userId.profile.name});
} else if (approvalsLength > 1 && !userIsRequesting) {
return `${approvalsLength} request approval`;
return this.$t('youAreRequestingApproval', {userCount: approvalsLength});
} else if (approvalsLength === 1 && userIsRequesting) {
return 'You are requesting approval';
return this.$t('youAreRequestingApproval');
}
},
userIsAdmin () {
+37 -15
View File
@@ -34,11 +34,10 @@
.svg-icon(v-html="icons[type]", :class="`icon-${type}`", v-once)
h3(v-once) {{$t('theseAreYourTasks', {taskType: $t(types[type].label)})}}
.small-text {{$t(`${type}sDesc`)}}
.sortable-tasks(
draggable(
ref="tasksList",
v-sortable='activeFilters[type].label !== "scheduled"',
@onsort='sorted',
data-sortableId
@update='sorted',
:options='{disabled: activeFilters[type].label === "scheduled"}',
)
task(
v-for="task in taskList",
@@ -64,7 +63,6 @@
@click.prevent.stop="togglePinned(ctx.item)"
)
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
</template>
<style lang="scss" scoped>
@@ -81,9 +79,20 @@
.reward-items {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@supports (display: grid) {
display: grid;
grid-column-gap: 16px;
grid-row-gap: 4px;
grid-template-columns: repeat(auto-fill, 94px);
}
@supports not (display: grid) {
display: flex;
flex-wrap: wrap;
& > div {
margin: 0 16px 4px 0;
}
}
}
.tasks-list {
@@ -235,7 +244,6 @@
import Task from './task';
import sortBy from 'lodash/sortBy';
import throttle from 'lodash/throttle';
import sortable from 'client/directives/sortable.directive';
import buyMixin from 'client/mixins/buy';
import { mapState, mapActions } from 'client/libs/store';
import shopItem from '../shops/shopItem';
@@ -252,6 +260,7 @@ import habitIcon from 'assets/svg/habit.svg';
import dailyIcon from 'assets/svg/daily.svg';
import todoIcon from 'assets/svg/todo.svg';
import rewardIcon from 'assets/svg/reward.svg';
import draggable from 'vuedraggable';
export default {
mixins: [buyMixin, notifications],
@@ -259,9 +268,7 @@ export default {
Task,
BuyQuestModal,
shopItem,
},
directives: {
sortable,
draggable,
},
props: ['type', 'isUser', 'searchText', 'selectedTags', 'taskListOverride', 'group'], // @TODO: maybe we should store the group on state?
data () {
@@ -334,6 +341,16 @@ export default {
user: 'user.data',
userPreferences: 'user.data.preferences',
}),
onUserPage () {
let onUserPage = Boolean(this.taskList.length) && (!this.taskListOverride || this.taskListOverride.length === 0);
if (!onUserPage) {
this.activateFilter('daily', this.types.daily.filters[0]);
this.types.reward.filters = [];
}
return onUserPage;
},
taskList () {
// @TODO: This should not default to user's tasks. It should require that you pass options in
const filter = this.activeFilters[this.type];
@@ -399,6 +416,7 @@ export default {
if (this.user.preferences.dailyDueDefaultView) {
this.activateFilter('daily', this.types.daily.filters[1]);
}
return this.user.preferences.dailyDueDefaultView;
},
quickAddPlaceholder () {
@@ -434,9 +452,12 @@ export default {
deep: true,
},
dailyDueDefaultView () {
if (this.user.preferences.dailyDueDefaultView) {
this.activateFilter('daily', this.types.daily.filters[1]);
}
if (!this.dailyDueDefaultView) return;
this.activateFilter('daily', this.types.daily.filters[1]);
},
quickAddFocused (newValue) {
if (newValue) this.quickAddRows = this.quickAddText.split('\n').length;
if (!newValue) this.quickAddRows = 1;
},
},
mounted () {
@@ -526,6 +547,7 @@ export default {
if (type === 'todo' && filter.label === 'complete2') {
this.loadCompletedTodos();
}
this.activeFilters[type] = filter;
},
setColumnBackgroundVisibility () {
+3 -1
View File
@@ -12,7 +12,7 @@ div(v-if='user.stats.lvl > 10')
drawer(
:title="$t('skillsTitle')",
v-if='user.stats.class && !user.preferences.disableClasses',
v-mousePosition="30",
v-mousePosition="30",
@mouseMoved="mouseMoved($event)",
:openStatus='openStatus',
@toggled='drawerToggled'
@@ -69,6 +69,7 @@ div(v-if='user.stats.lvl > 10')
.details {
text-align: left;
padding-top: .5em;
padding-right: .1em;
.img {
display: inline-block;
@@ -129,6 +130,7 @@ div(v-if='user.stats.lvl > 10')
.title {
font-weight: bold;
margin-bottom: .2em;
}
}
+11 -1
View File
@@ -135,8 +135,11 @@
color: $gray-10;
font-weight: normal;
margin-bottom: 0px;
margin-right: 15px;
line-height: 1.43;
font-size: 14px;
min-width: 0px;
overflow-wrap: break-word;
&.has-notes {
padding-bottom: 4px;
@@ -159,6 +162,7 @@
.dropdown-icon {
width: 4px;
height: 16px;
margin-right: 10px;
color: $gray-100 !important;
}
@@ -212,7 +216,9 @@
.task-notes {
color: $gray-100;
font-style: normal;
padding-right: 6px;
padding-right: 20px;
min-width: 0px;
overflow-wrap: break-word;
&.has-checklist {
padding-bottom: 8px;
@@ -227,6 +233,7 @@
background: $white;
border: 1px solid transparent;
transition-duration: 0.15;
min-width: 0px;
&.no-right-border {
border-right: none !important;
@@ -271,6 +278,8 @@
min-height: 0px;
width: 100%;
margin-left: 8px;
padding-right: 20px;
overflow-wrap: break-word;
&-done {
color: $gray-300;
@@ -284,6 +293,7 @@
.custom-control-description {
margin-left: 6px;
padding-top: 0px;
min-width: 0px;
}
}
+53 -20
View File
@@ -13,7 +13,7 @@
type="text", :class="[`${cssClass}-modal-input`]",
required, v-model="task.text",
autofocus, spellcheck="true",
:disabled="groupAccessRequiredAndOnPersonalPage"
:disabled="groupAccessRequiredAndOnPersonalPage || challengeAccessRequired"
)
.form-group
label(v-once) {{ $t('notes') }}
@@ -26,7 +26,11 @@
.option(v-if="checklistEnabled")
label(v-once) {{ $t('checklist') }}
br
div(v-sortable='true', @onsort='sortedChecklist')
draggable(
v-model="checklist",
:options="{handle: '.grippy', filter: '.task-dropdown'}",
@update="sortedChecklist"
)
.inline-edit-input-group.checklist-group.input-group(v-for="(item, $index) in checklist")
span.grippy
input.inline-edit-input.checklist-item.form-control(type="text", v-model="item.text")
@@ -34,12 +38,12 @@
.svg-icon.destroy-icon(v-html="icons.destroy")
input.inline-edit-input.checklist-item.form-control(type="text", :placeholder="$t('newChecklistItem')", @keydown.enter="addChecklistItem($event)", v-model="newChecklistItem")
.d-flex.justify-content-center(v-if="task.type === 'habit'")
.option-item(:class="optionClass(task.up === true)", @click="task.up = !task.up")
.option-item(:class="optionClass(task.up === true)", @click="toggleUpDirection()")
.option-item-box
.task-control.habit-control(:class="controlClass.up + '-control-habit'")
.svg-icon.positive(v-html="icons.positive")
.option-item-label(v-once) {{ $t('positive') }}
.option-item(:class="optionClass(task.down === true)", @click="task.down = !task.down")
.option-item(:class="optionClass(task.down === true)", @click="toggleDownDirection()")
.option-item-box
.task-control.habit-control(:class="controlClass.down + '-control-habit'")
.svg-icon.negative(v-html="icons.negative")
@@ -49,19 +53,19 @@
span.float-left {{ $t('difficulty') }}
// @TODO .svg-icon.info-icon(v-html="icons.information")
.d-flex.justify-content-center
.option-item(:class="optionClass(task.priority === 0.1)", @click="task.priority = 0.1")
.option-item(:class="optionClass(task.priority === 0.1)", @click="setDifficulty(0.1)")
.option-item-box
.svg-icon.difficulty-trivial-icon(v-html="icons.difficultyTrivial")
.option-item-label(v-once) {{ $t('trivial') }}
.option-item(:class="optionClass(task.priority === 1)", @click="task.priority = 1")
.option-item(:class="optionClass(task.priority === 1)", @click="setDifficulty(1)")
.option-item-box
.svg-icon.difficulty-normal-icon(v-html="icons.difficultyNormal")
.option-item-label(v-once) {{ $t('easy') }}
.option-item(:class="optionClass(task.priority === 1.5)", @click="task.priority = 1.5")
.option-item(:class="optionClass(task.priority === 1.5)", @click="setDifficulty(1.5)")
.option-item-box
.svg-icon.difficulty-medium-icon(v-html="icons.difficultyMedium")
.option-item-label(v-once) {{ $t('medium') }}
.option-item(:class="optionClass(task.priority === 2)", @click="task.priority = 2")
.option-item(:class="optionClass(task.priority === 2)", @click="setDifficulty(2)")
.option-item-box
.svg-icon.difficulty-hard-icon(v-html="icons.difficultyHard")
.option-item-label(v-once) {{ $t('hard') }}
@@ -72,28 +76,33 @@
:clearButton='true',
clearButtonIcon='category-select',
:clearButtonText='$t("clear")',
:todayButton='true',
:todayButton='!challengeAccessRequired',
todayButtonIcon='category-select',
:todayButtonText='$t("today")',
:disabled-picker='challengeAccessRequired'
)
.option(v-if="task.type === 'daily'")
label(v-once) {{ $t('startDate') }}
datepicker.d-inline-block(
v-model="task.startDate",
:clearButton='false',
:todayButton='true',
:todayButton='!challengeAccessRequired',
todayButtonIcon='category-select',
:todayButtonText='$t("today")',
:disabled-picker='challengeAccessRequired'
)
.option(v-if="task.type === 'daily'")
.form-group
label(v-once) {{ $t('repeats') }}
b-dropdown(:text="$t(task.frequency)")
b-dropdown-item(v-for="frequency in ['daily', 'weekly', 'monthly', 'yearly']", :key="frequency", @click="task.frequency = frequency", :class="{active: task.frequency === frequency}")
b-dropdown-item(v-for="frequency in ['daily', 'weekly', 'monthly', 'yearly']",
:key="frequency", @click="task.frequency = frequency",
:disabled='challengeAccessRequired',
:class="{active: task.frequency === frequency}")
| {{ $t(frequency) }}
.form-group
label(v-once) {{ $t('repeatEvery') }}
input(type="number", v-model="task.everyX", min="0", required)
input(type="number", v-model="task.everyX", min="0", required, :disabled='challengeAccessRequired')
| {{ repeatSuffix }}
br
template(v-if="task.frequency === 'weekly'")
@@ -102,7 +111,7 @@
:key="dayNumber",
)
label.custom-control.custom-checkbox
input.custom-control-input(type="checkbox", v-model="task.repeat[day]")
input.custom-control-input(type="checkbox", v-model="task.repeat[day]", :disabled='challengeAccessRequired')
span.custom-control-indicator
span.custom-control-description(v-once) {{ weekdaysMin(dayNumber) }}
template(v-if="task.frequency === 'monthly'")
@@ -118,7 +127,7 @@
.tags-select.option(v-if="isUserTask")
.tags-inline
label(v-once) {{ $t('tags') }}
.category-wrap(@click="showTagsSelect = !showTagsSelect", v-bind:class="{ active: showTagsSelect }")
.category-wrap(@click="toggleTagSelect()", v-bind:class="{ active: showTagsSelect }")
span.category-select(v-if='task.tags && task.tags.length === 0')
.tags-none {{$t('none')}}
.dropdown-toggle
@@ -137,7 +146,7 @@
.option(v-if="task.type === 'daily' && isUserTask && purpose === 'edit'")
.form-group
label(v-once) {{ $t('restoreStreak') }}
input(type="number", v-model="task.streak", min="0", required)
input(type="number", v-model="task.streak", min="0", required, :disabled='challengeAccessRequired')
.option.group-options(v-if='groupId')
label(v-once) Assigned To
@@ -461,6 +470,7 @@
}
&:hover {
cursor: text;
.destroy-icon {
display: inline-block;
color: $gray-200;
@@ -513,11 +523,11 @@
import TagsPopup from './tagsPopup';
import { mapGetters, mapActions, mapState } from 'client/libs/store';
import toggleSwitch from 'client/components/ui/toggleSwitch';
import sortable from 'client/directives/sortable.directive';
import clone from 'lodash/clone';
import Datepicker from 'vuejs-datepicker';
import moment from 'moment';
import uuid from 'uuid';
import draggable from 'vuedraggable';
import informationIcon from 'assets/svg/information.svg';
import difficultyTrivialIcon from 'assets/svg/difficulty-trivial.svg';
@@ -534,9 +544,7 @@ export default {
TagsPopup,
Datepicker,
toggleSwitch,
},
directives: {
sortable,
draggable,
},
// purpose is either create or edit, task is the task created or edited
props: ['task', 'purpose', 'challengeId', 'groupId'],
@@ -598,12 +606,21 @@ export default {
dayMapping: 'constants.DAY_MAPPING',
}),
groupAccessRequiredAndOnPersonalPage () {
if (!this.groupId && this.task.group.id) return true;
if (!this.groupId && this.task.group && this.task.group.id) return true;
return false;
},
checklistEnabled () {
return ['daily', 'todo'].indexOf(this.task.type) > -1 && !this.isOriginalChallengeTask;
},
isChallengeTask () {
return Boolean(this.task.challenge && this.task.challenge.id);
},
onUserPage () {
return !this.challengeId && !this.groupId;
},
challengeAccessRequired () {
return this.onUserPage && this.isChallengeTask;
},
isOriginalChallengeTask () {
let isUserChallenge = Boolean(this.task.userId);
return !isUserChallenge && (this.challengeId || this.task.challenge && this.task.challenge.id);
@@ -682,6 +699,22 @@ export default {
closeTagsPopup () {
this.showTagsSelect = false;
},
setDifficulty (level) {
if (this.challengeAccessRequired) return;
this.task.priority = level;
},
toggleUpDirection () {
if (this.challengeAccessRequired) return;
this.task.up = !this.task.up;
},
toggleDownDirection () {
if (this.challengeAccessRequired) return;
this.task.down = !this.task.down;
},
toggleTagSelect () {
if (this.challengeAccessRequired) return;
this.showTagsSelect = !this.showTagsSelect;
},
sortedChecklist (data) {
let sorting = clone(this.task.checklist);
let movingItem = sorting[data.oldIndex];
+4 -1
View File
@@ -407,10 +407,13 @@ export default {
this.newTag = null;
},
removeTag (index, key) {
const tagId = this.tagsSnap[key][index].id;
const indexInSelected = this.selectedTags.indexOf(tagId);
if (indexInSelected !== -1) this.$delete(this.selectedTags, indexInSelected);
this.$delete(this.tagsSnap[key], index);
},
saveTags () {
if (this.newTag) this.addTag();
if (this.newTag) this.addTag(null, 'tags');
this.tagsByType.user.tags = this.tagsSnap.tags;
this.tagsByType.challenges.tags = this.tagsSnap.challenges;
+2 -2
View File
@@ -27,8 +27,8 @@
@media screen and (min-width: 1241px) {
max-width: 978px;
// 16.67% is the width of the .col-2 sidebar
left: calc((100% + 16.67% - 978px) / 2);
// 236px is the width of the .standard-sidebar
left: calc((100% + 236px - 978px) / 2);
right: 0%;
}
}
+12 -1
View File
@@ -631,6 +631,18 @@ export default {
},
};
},
mounted () {
this.$root.$on('habitica:show-profile', (data) => {
if (!data.user || !data.startingPage) return;
// @TODO: We may be able to remove the need for store
this.$store.state.profileUser = data.user;
this.$store.state.profileOptions.startingPage = data.startingPage;
this.$root.$emit('bv::show::modal', 'profile');
});
},
destroyed () {
this.$root.$off('habitica:show-profile');
},
computed: {
...mapState({
userLoggedIn: 'user.data',
@@ -740,7 +752,6 @@ export default {
let curVal = this.user.profile[key];
if (!curVal || this.editingProfile[key].toString() !== curVal.toString()) {
values[`profile.${key}`] = value;
this.$set(this.userLoggedIn.profile, key, value);
this.$set(this.user.profile, key, value);
}
});
@@ -11,8 +11,11 @@ export default {
profile,
},
mounted () {
this.$store.state.profileUser = {};
this.$root.$emit('bv::show::modal', 'profile');
// @TODO: Do we need this page?
this.$root.$emit('habitica:show-profile', {
user: {},
startingPage: 'profile',
});
},
};
</script>
@@ -78,6 +78,7 @@ export default {
methods: {
async close () {
this.$root.$emit('bv::hide::modal', 'yesterdaily');
this.$emit('run-cron');
},
},
};
@@ -1,46 +0,0 @@
import Sortable from 'sortablejs';
import uuid from 'uuid';
let emit = (vNode, eventName, data) => {
let handlers = vNode.data && vNode.data.on ||
vNode.componentOptions && vNode.componentOptions.listeners;
if (handlers && handlers[eventName]) {
handlers[eventName].fns(data);
}
};
let sortableReferences = {};
function createSortable (el, vNode) {
let sortableRef = Sortable.create(el, {
filter: '.task-dropdown', // do not make the tasks dropdown draggable or it won't work
onSort: (evt) => {
emit(vNode, 'onsort', {
oldIndex: evt.oldIndex,
newIndex: evt.newIndex,
});
},
});
let uniqueId = uuid();
sortableReferences[uniqueId] = sortableRef;
el.dataset.sortableId = uniqueId;
}
export default {
bind (el, binding, vNode) {
createSortable(el, vNode);
},
unbind (el) {
if (sortableReferences[el.dataset.sortableId]) sortableReferences[el.dataset.sortableId].destroy();
},
update (el, vNode) {
if (sortableReferences[el.dataset.sortableId] && !vNode.value) {
sortableReferences[el.dataset.sortableId].destroy();
delete sortableReferences[el.dataset.sortableId];
return;
}
if (!sortableReferences[el.dataset.sortableId]) createSortable(el, vNode);
},
};
@@ -0,0 +1,21 @@
// @TODO: How do we require data or make this functional
import debounce from 'lodash/debounce';
export default {
watch: {
searchTerm: debounce(function searchTerm (newSearch) {
this.challengeMemberSearchMixin_searchChallengeMember(newSearch);
}, 500),
members () {
this.memberResults = this.members;
},
},
methods: {
async challengeMemberSearchMixin_searchChallengeMember (search) { // eslint-disable-line
this.memberResults = await this.$store.dispatch('members:getChallengeMembers', {
challengeId: this.challengeId,
searchTerm: search,
});
},
},
};
+1 -1
View File
@@ -52,7 +52,7 @@ export default {
}));
},
streak (val) {
this.notify(`${this.$t('streaks')}: ${val}`, 'streak', 'glyphicon glyphicon-repeat');
this.notify(`${val}`, 'streak');
},
text (val, onClick) {
if (!val) return;
+6 -1
View File
@@ -169,12 +169,14 @@ export default {
this.amazonPayments.gift = data.gift;
this.amazonPayments.type = data.type;
this.$root.$emit('bv::show::modal', 'amazon-payment');
this.$root.$emit('habitica::pay-with-amazon', this.amazonPayments);
},
async cancelSubscription (config) {
if (config && config.group && !confirm(this.$t('confirmCancelGroupPlan'))) return;
if (!confirm(this.$t('sureCancelSub'))) return;
this.loading = true;
let group;
if (config && config.group) {
group = config.group;
@@ -203,6 +205,9 @@ export default {
let cancelUrl = `/${paymentMethod}/subscribe/cancel?${encodeParams(queryParams)}`;
await axios.get(cancelUrl);
this.loading = false;
// Success
alert(this.$t('paypalCanceled'));
this.$router.push('/');
+9 -1
View File
@@ -33,7 +33,7 @@ export function buyQuestItem (store, params) {
return {
result: opResult,
httpCall: axios.post(`/api/v3/user/buy/${params.key}`, {type: 'quest'}),
httpCall: axios.post(`/api/v3/user/buy/${params.key}`, {type: 'quest', quantity}),
};
}
@@ -41,6 +41,14 @@ async function buyArmoire (store, params) {
const quantity = params.quantity || 1;
let armoire = content.armoire;
buyOp(store.state.user.data, {
params: {
key: 'armoire',
},
type: 'armoire',
quantity,
});
// We need the server result because Armoire has random item in the result
let result = await axios.post('/api/v3/user/buy/armoire', {
type: 'armoire',
+2
View File
@@ -81,6 +81,8 @@ export async function changeClass (store, params) {
const user = store.state.user.data;
changeClassOp(user, params);
user.flags.classSelected = true;
let response = await axios.post(`/api/v3/user/change-class?class=${params.query.class}`);
return response.data.data;
}
+10 -10
View File
@@ -386,8 +386,8 @@
"armorSpecialDandySuitNotes": "С това определено ще изглеждате като някой от висшата класа! Увеличава усета с <%= per %>.",
"armorSpecialSamuraiArmorText": "Самурайска броня",
"armorSpecialSamuraiArmorNotes": "Частите на тази здрава броня са свързани с изящни копринени нишки. Увеличава усета с <%= per %>.",
"armorSpecialTurkeyArmorBaseText": "Turkey Armor",
"armorSpecialTurkeyArmorBaseNotes": "Keep your drumsticks warm and cozy in this feathery armor! Confers no benefit.",
"armorSpecialTurkeyArmorBaseText": "Пуешка броня",
"armorSpecialTurkeyArmorBaseNotes": "С тази пухена броня ще Ви бъде много топло и удобно! Не променя показателите.",
"armorSpecialYetiText": "Мантия на укротител на йетита",
"armorSpecialYetiNotes": "Пухкава и жестока. Увеличава якостта с <%= con %>. Ограничена серия: Зимна екипировка 2013-2014 г.",
"armorSpecialSkiText": "Анорак на ски-убиец",
@@ -584,8 +584,8 @@
"armorMystery201707Notes": "Тази броня ще Ви помогне да се слеете с морските същества, докато изпълнявате подводните си мисии или преживявате други приключения под водата. Не променя показателите. Предмет за абонати: юли 2017 г.",
"armorMystery201710Text": "Облекло на надменно дяволче",
"armorMystery201710Notes": "Люспесто, лъскаво и здраво! Не променя показателите. Предмет за абонати: октомври 2017 г.",
"armorMystery201711Text": "Carpet Rider Outfit",
"armorMystery201711Notes": "This cozy sweater set will help keep you warm as you ride through the sky! Confers no benefit. November 2017 Subscriber Item.",
"armorMystery201711Text": "Облекло на килимния летец",
"armorMystery201711Notes": "Този удобен пуловер ще Ви държи топло докато се носите из облаците! Не променя показателите. Предмет за абонати: ноември 2017 г.",
"armorMystery301404Text": "Изтънчен костюм",
"armorMystery301404Notes": "Спретнат и елегантен! Не променя показателите. Предмет за абонати: февруари 3015 г.",
"armorMystery301703Text": "Изтънчена паунова рокля",
@@ -740,8 +740,8 @@
"headSpecialKabutoNotes": "Този шлем е функционален и красив! Враговете Ви ще се разсеят, тъй като ще са заети да му се възхищават. Увеличава интелигентността с <%= int %>.",
"headSpecialNamingDay2017Text": "Царствено лилав грифонски шлем",
"headSpecialNamingDay2017Notes": "Честит имен ден! Носете този яростен пернат шлем, когато празнувате името на Хабитика. Не променя показателите.",
"headSpecialTurkeyHelmBaseText": "Turkey Helm",
"headSpecialTurkeyHelmBaseNotes": "Your Turkey Day look will be complete when you don this beaked helm! Confers no benefit.",
"headSpecialTurkeyHelmBaseText": "Пуешки шлем",
"headSpecialTurkeyHelmBaseNotes": "Облеклото Ви за деня на пуйката ще бъде завършено с този шлем с клюн! Не променя показателите.",
"headSpecialNyeText": "Абсурдна купонджийска шапка",
"headSpecialNyeNotes": "Получихте абсурдна купонджийска шапка! Носете я с гордост, когато посрещате Нова година! Не променя показателите.",
"headSpecialYetiText": "Шлем на укротител на йетита",
@@ -1252,8 +1252,8 @@
"backSpecialSnowdriftVeilNotes": "С този прозрачен воал ще изглеждате така, сякаш Ви обгръща изящен снежен облак! Не променя показателите.",
"backSpecialAetherCloakText": "Етерна мантия",
"backSpecialAetherCloakNotes": "Тази мантия някога е принадлежала на самата Изгубената класова повелителка. Увеличава усета с <%= per %>.",
"backSpecialTurkeyTailBaseText": "Turkey Tail",
"backSpecialTurkeyTailBaseNotes": "Wear your noble Turkey Tail with pride while you celebrate! Confers no benefit.",
"backSpecialTurkeyTailBaseText": "Пуешка опашка",
"backSpecialTurkeyTailBaseNotes": "Носете гордо пуешката си опашка, докато празнувате! Не променя показателите.",
"body": "Аксесоар за тяло",
"bodyCapitalized": "Аксесоар за тяло",
"bodyBase0Text": "Няма аксесоар за тяло",
@@ -1284,8 +1284,8 @@
"bodyMystery201705Notes": "Тези свити криле не просто изглеждат страхотно — с тях ще имате бързината и ловкостта на грифон! Не променя показателите. Предмет за абонати: май 2017 г.",
"bodyMystery201706Text": "Наметало на парцалив пират",
"bodyMystery201706Notes": "Това наметало има тайни джобове, където можете да скриете всичкото злато, което измъкнете от задачите си. Не променя показателите. Предмет за абонати: юни 2017 г.",
"bodyMystery201711Text": "Carpet Rider Scarf",
"bodyMystery201711Notes": "This soft knitted scarf looks quite majestic blowing in the wind. Confers no benefit. November 2017 Subscriber Item.",
"bodyMystery201711Text": "Шал на килимния летец",
"bodyMystery201711Notes": "Този мек плетен шал изглежда доста впечатляващо, когато се развява от вятъра. Не променя показателите. Предмет за абонати: ноември 2017 г.",
"headAccessory": "аксесоар за глава",
"headAccessoryCapitalized": "Аксесоар за глава",
"accessories": "Аксесоари",
+1 -1
View File
@@ -135,7 +135,7 @@
"mysterySet201708": "Комплект на воина на лавата",
"mysterySet201709": "Комплект на ученика-магьосник",
"mysterySet201710": "Комплект на надменното дяволче",
"mysterySet201711": "Carpet Rider Set",
"mysterySet201711": "Комплект на килимния летец",
"mysterySet301404": "Стандартен изтънчен комплект",
"mysterySet301405": "Комплект изтънчени принадлежности",
"mysterySet301703": "Изтънчен паунов комплект",
+1 -1
View File
@@ -29,7 +29,7 @@
"either": "Beides",
"createChallenge": "Wettbewerb erstellen",
"createChallengeAddTasks": "Wettbewerbsaufgabe hinzufügen",
"createChallengeCloneTasks": "Clone Challenge Tasks",
"createChallengeCloneTasks": "Wettbewerbsaufgaben kopieren",
"addTaskToChallenge": "Aufgabe hinzufügen",
"discard": "Verwerfen",
"challengeTitle": "Titel des Wettbewerbs",
+3 -3
View File
@@ -158,9 +158,9 @@
"questEggHippoText": "Nilpferd",
"questEggHippoMountText": "Nilpferd",
"questEggHippoAdjective": "ein glückliches",
"questEggYarnText": "Yarn",
"questEggYarnMountText": "Flying Carpet",
"questEggYarnAdjective": "woolen",
"questEggYarnText": "Wollknäuel",
"questEggYarnMountText": "Fliegender Teppich",
"questEggYarnAdjective": "wolliges",
"eggNotes": "Finde ein Schlüpfelixier, das Du über dieses Ei gießen kannst, damit ein <%= eggAdjective(locale) %> <%= eggText(locale) %> schlüpfen kann.",
"hatchingPotionBase": "Normales",
"hatchingPotionWhite": "Weißes",
+1 -1
View File
@@ -6,7 +6,7 @@
"questsForSale": "Kaufbare Quests",
"petQuests": "Haustier- und Reittier-Quests",
"unlockableQuests": "Freischaltbare Quests",
"goldQuests": "Masterclasser Quest Lines",
"goldQuests": "Klassenmeister-Questreihen",
"questDetails": "Quest-Details",
"questDetailsTitle": "Quest-Details",
"questDescription": "Quests erlauben es Spielern, sich gemeinsam mit den Gruppenmitgliedern auf Langzeit-Ziele im Spiel zu konzentrieren. ",
+3 -3
View File
@@ -544,10 +544,10 @@
"questLostMasterclasser4DropBackAccessory": "Aether Cloak (Back Accessory)",
"questLostMasterclasser4DropWeapon": "Aether Crystals (Two-Handed Weapon)",
"questLostMasterclasser4DropMount": "Invisible Aether Mount",
"questYarnText": "A Tangled Yarn",
"questYarnText": "Ein verheddertes Knäuel",
"questYarnNotes": "Its such a pleasant day that you decide to take a walk through the Taskan Countryside. As you pass by its famous yarn shop, a piercing scream startles the birds into flight and scatters the butterflies into hiding. You run towards the source and see @Arcosine running up the path towards you. Behind him, a horrifying creature consisting of yarn, pins, and knitting needles is clicking and clacking ever closer.<br><br>The shopkeepers race after him, and @stefalupagus grabs your arm, out of breath. \"Looks like all of his unfinished projects\" <em>gasp gasp</em> \"have transformed the yarn from our Yarn Shop\" <em>gasp gasp</em> \"into a tangled mass of Yarnghetti!\"<br><br>\"Sometimes, life gets in the way and a project is abandoned, becoming ever more tangled and confused,\" says @khdarkwolf. \"The confusion can even spread to other projects, until there are so many half-finished works running around that no one gets anything done!\"<br><br>Its time to make a choice: complete your stalled projects… or decide to unravel them for good. Either way, you'll have to increase your productivity quickly before the Dread Yarnghetti spreads confusion and discord to the rest of Habitica!",
"questYarnCompletion": "With a feeble swipe of a pin-riddled appendage and a weak roar, the Dread Yarnghetti finally unravels into a pile of yarn balls.<br><br>\"Take care of this yarn,\" shopkeeper @JinjooHat says, handing them to you. \"If you feed them and care for them properly, they'll grow into new and exciting projects that just might make your heart take flight…\"",
"questYarnBoss": "The Dread Yarnghetti",
"questYarnDropYarnEgg": "Yarn (Egg)",
"questYarnUnlockText": "Unlocks purchasable Yarn eggs in the Market"
"questYarnDropYarnEgg": "Wollknäuel (Ei)",
"questYarnUnlockText": "Ermöglicht den Kauf von Wollknäueleiern auf dem Marktplatz"
}
+2 -1
View File
@@ -129,5 +129,6 @@
"locationRequired": "Location of challenge is required ('Add to')",
"categoiresRequired": "One or more categories must be selected",
"viewProgressOf": "View Progress Of",
"selectMember": "Select Member"
"selectMember": "Select Member",
"confirmKeepChallengeTasks": "Do you want to keep challenge tasks?"
}
+1
View File
@@ -163,6 +163,7 @@
"dieText": "You've lost a Level, all your Gold, and a random piece of Equipment. Arise, Habiteer, and try again! Curb those negative Habits, be vigilant in completion of Dailies, and hold death at arm's length with a Health Potion if you falter!",
"sureReset": "Are you sure? This will reset your character's class and allocated points (you'll get them all back to re-allocate), and costs 3 Gems.",
"purchaseFor": "Purchase for <%= cost %> Gems?",
"purchaseForHourglasses": "Purchase for <%= cost %> Hourglasses?",
"notEnoughMana": "Not enough mana.",
"invalidTarget": "You can't cast a skill on that.",
"youCast": "You cast <%= spell %>.",
+1 -1
View File
@@ -32,7 +32,7 @@
"contribModal": "<%= name %>, you awesome person! You're now a tier <%= level %> contributor for helping Habitica. See",
"contribLink": "what prizes you've earned for your contribution!",
"contribName": "Contributor",
"contribText": "Has contributed to Habitica (code, design, pixel art, legal advice, docs, etc). Want this badge? <a href='http://habitica.wikia.com/wiki/Contributing_to_Habitica' target='_blank'>Read more.</a>",
"contribText": "Has contributed to Habitica, whether via code, art, music, writing, or other methods. To learn more, join the Aspiring Legends Guild!",
"readMore": "Read More",
"kickstartName": "Kickstarter Backer - $<%= key %> Tier",
"kickstartText": "Backed the Kickstarter Project",
+2 -1
View File
@@ -30,6 +30,7 @@
"companyAbout": "How It Works",
"companyBlog": "Blog",
"devBlog": "Developer Blog",
"companyContribute": "Contribute",
"companyDonate": "Donate",
"companyPrivacy": "Privacy",
"companyTerms": "Terms",
@@ -252,7 +253,7 @@
"missingNewPassword": "Missing new password.",
"invalidEmailDomain": "You cannot register with emails with the following domains: <%= domains %>",
"wrongPassword": "Wrong password.",
"incorrectDeletePhrase": "Please type DELETE in all caps to delete your account.",
"incorrectDeletePhrase": "Please type <%= magicWord %> in all caps to delete your account.",
"notAnEmail": "Invalid email address.",
"emailTaken": "Email address is already used in an account.",
"newEmailRequired": "Missing new email address.",
+2 -2
View File
@@ -111,8 +111,8 @@
"achievementDilatory": "Savior of Dilatory",
"achievementDilatoryText": "Helped defeat the Dread Drag'on of Dilatory during the 2014 Summer Splash Event!",
"costumeContest": "Costume Contestant",
"costumeContestText": "Participated in the Habitoween Costume Contest. See some of the entries <a href='http://blog.habitrpg.com/tagged/cosplay' target='_blank'>on the Habitica blog</a>!",
"costumeContestTextPlural": "Participated in <%= count %> Habitoween Costume Contests. See some of the entries <a href='http://blog.habitrpg.com/tagged/cosplay' target='_blank'>on the Habitica blog</a>!",
"costumeContestText": "Participated in the Habitoween Costume Contest. See some of the awesome entries at blog.habitrpg.com!",
"costumeContestTextPlural": "Participated in <%= count %> Habitoween Costume Contests. See some of the awesome entries at blog.habitrpg.com!",
"memberSince": "- Member since",
"lastLoggedIn": "- Last logged in",
"notPorted": "This feature is not yet ported from the original site.",

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