Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97e1d75dce | ||
|
|
52bf20c27d | ||
|
|
5dbaf39aba | ||
|
|
66d402c985 | ||
|
|
8048146223 | ||
|
|
e2c07e458d | ||
|
|
90a9e8e192 | ||
|
|
f8039f48a6 | ||
|
|
04337f8e83 | ||
|
|
45297f8bf9 | ||
|
|
6f112c29f2 | ||
|
|
4d1edb363c | ||
|
|
4e303cc592 | ||
|
|
798a975185 | ||
|
|
eb2b46fc5d | ||
|
|
29854d3bdb | ||
|
|
f8751b002c | ||
|
|
cd545e08d5 | ||
|
|
f69bb4f023 | ||
|
|
847081d2b2 | ||
|
|
8112d46ea4 | ||
|
|
d13bded647 | ||
|
|
1de4ab3612 | ||
|
|
f9f22f313f | ||
|
|
cbef83c14a | ||
|
|
605a5a1d5c | ||
|
|
2d5d786c8e | ||
|
|
3e92bb22fa | ||
|
|
79829ca128 | ||
|
|
adaa1d9a3e | ||
|
|
679459b83b | ||
|
|
ad76ab1315 | ||
|
|
15eb8db925 | ||
|
|
0d58fb0fd3 |
@@ -14,7 +14,7 @@ files:
|
||||
owner: root
|
||||
group: users
|
||||
content: |
|
||||
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@3
|
||||
$(ls -td /opt/elasticbeanstalk/node-install/node-* | head -1)/bin/npm install -g npm@4
|
||||
container_commands:
|
||||
01_makeBabel:
|
||||
command: "touch /tmp/.babel.json"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- '4.3.1'
|
||||
- '6'
|
||||
before_install:
|
||||
- npm install -g npm@3
|
||||
- npm install -g npm@4
|
||||
- if [ $REQUIRES_SERVER ]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10; echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list; sudo apt-get update; sudo apt-get install mongodb-org-server; fi
|
||||
before_script:
|
||||
- npm run test:build
|
||||
|
||||
@@ -17,7 +17,7 @@ RUN apt-get install -y \
|
||||
python
|
||||
|
||||
# Install NodeJS
|
||||
RUN curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
|
||||
RUN curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -
|
||||
RUN apt-get install -y nodejs
|
||||
|
||||
# Clean up package management
|
||||
@@ -25,7 +25,7 @@ RUN apt-get clean
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g npm@3
|
||||
RUN npm install -g npm@4
|
||||
RUN npm install -g gulp grunt-cli bower
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
|
||||
@@ -318,7 +318,7 @@ gulp.task('test:api-v3:integration:watch', () => {
|
||||
|
||||
gulp.task('test:api-v3:integration:separate-server', (done) => {
|
||||
let runner = exec(
|
||||
testBin('mocha test/api/v3/integration --recursive', 'LOAD_SERVER=0'),
|
||||
testBin('mocha test/api/v3/integration --recursive --require ./test/helpers/start-server', 'LOAD_SERVER=0'),
|
||||
{maxBuffer: 500 * 1024},
|
||||
(err, stdout, stderr) => done(err)
|
||||
);
|
||||
|
||||
659
npm-shrinkwrap.json
generated
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.59.0",
|
||||
"version": "3.60.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@slack/client": "3.6.0",
|
||||
@@ -15,8 +15,10 @@
|
||||
"aws-sdk": "^2.0.25",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-loader": "^6.0.0",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-regenerator": "^6.16.1",
|
||||
"babel-polyfill": "^6.6.1",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-register": "^6.6.0",
|
||||
@@ -29,7 +31,7 @@
|
||||
"compression": "^1.6.1",
|
||||
"connect-ratelimit": "0.0.7",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.3",
|
||||
"coupon-code": "^0.4.5",
|
||||
"css-loader": "^0.23.1",
|
||||
"csv-stringify": "^1.0.2",
|
||||
"cwait": "^1.0.0",
|
||||
@@ -75,7 +77,7 @@
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"mongoose": "4.6.6",
|
||||
"mongoose": "^4.7.1",
|
||||
"mongoose-id-autoinc": "~2013.7.14-4",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "~0.8.2",
|
||||
@@ -126,8 +128,8 @@
|
||||
},
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": "^4.3.1",
|
||||
"npm": "^3.8.9"
|
||||
"node": "^6.9.1",
|
||||
"npm": "^4.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
|
||||
@@ -10,13 +10,11 @@ describe('payments - amazon - #createOrderReferenceId', () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
it('verifies billingAgreementId', async (done) => {
|
||||
try {
|
||||
await user.post(endpoint);
|
||||
} catch (e) {
|
||||
// Parameter AWSAccessKeyId cannot be empty.
|
||||
expect(e.error).to.eql('BadRequest');
|
||||
done();
|
||||
}
|
||||
it('verifies billingAgreementId', async () => {
|
||||
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'Missing req.body.billingAgreementId',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -145,6 +145,14 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.dateUpdated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(recipient.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('does not change plan.customerId if it already exists', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
data.customerId = 'purchaserCustomerId';
|
||||
|
||||
@@ -628,24 +628,89 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a private group when the last member leaves', async () => {
|
||||
party.memberCount = 1;
|
||||
|
||||
it('deletes a private party when the last member leaves', async () => {
|
||||
await party.leave(participatingMember);
|
||||
await party.leave(questLeader);
|
||||
await party.leave(nonParticipatingMember);
|
||||
await party.leave(undecidedMember);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not delete a public group when the last member leaves', async () => {
|
||||
party.memberCount = 1;
|
||||
party.privacy = 'public';
|
||||
|
||||
await party.leave(participatingMember);
|
||||
await party.leave(questLeader);
|
||||
await party.leave(nonParticipatingMember);
|
||||
await party.leave(undecidedMember);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.exist;
|
||||
});
|
||||
|
||||
it('does not delete a private party when the member count reaches zero if there are still members', async () => {
|
||||
party.memberCount = 1;
|
||||
|
||||
await party.leave(participatingMember);
|
||||
|
||||
party = await Group.findOne({_id: party._id});
|
||||
expect(party).to.exist;
|
||||
});
|
||||
|
||||
it('deletes a private guild when the last member leaves', async () => {
|
||||
let guild = new Group({
|
||||
name: 'test guild',
|
||||
type: 'guild',
|
||||
memberCount: 1,
|
||||
});
|
||||
|
||||
let leader = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
guild.leader = leader._id;
|
||||
|
||||
await Promise.all([
|
||||
guild.save(),
|
||||
leader.save(),
|
||||
]);
|
||||
|
||||
await guild.leave(leader);
|
||||
|
||||
guild = await Group.findOne({_id: guild._id});
|
||||
expect(guild).to.not.exist;
|
||||
});
|
||||
|
||||
it('does not delete a private guild when the member count reaches zero if there are still members', async () => {
|
||||
let guild = new Group({
|
||||
name: 'test guild',
|
||||
type: 'guild',
|
||||
memberCount: 1,
|
||||
});
|
||||
|
||||
let leader = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
let member = new User({
|
||||
guilds: [guild._id],
|
||||
});
|
||||
|
||||
guild.leader = leader._id;
|
||||
|
||||
await Promise.all([
|
||||
guild.save(),
|
||||
leader.save(),
|
||||
member.save(),
|
||||
]);
|
||||
|
||||
await guild.leave(member);
|
||||
|
||||
guild = await Group.findOne({_id: guild._id});
|
||||
expect(guild).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendChat', () => {
|
||||
@@ -1061,8 +1126,45 @@ describe('Group Model', () => {
|
||||
[nonParticipatingMember._id]: false,
|
||||
[undecidedMember._id]: null,
|
||||
};
|
||||
});
|
||||
|
||||
sandbox.spy(User, 'update');
|
||||
describe('user update retry failures', () => {
|
||||
let successfulMock = {
|
||||
exec: () => {
|
||||
return Promise.resolve({raw: 'sucess'});
|
||||
},
|
||||
};
|
||||
let failedMock = {
|
||||
exec: () => {
|
||||
return Promise.reject(new Error('error'));
|
||||
},
|
||||
};
|
||||
|
||||
it('doesn\'t retry successful operations', async () => {
|
||||
sandbox.stub(User, 'update').returns(successfulMock);
|
||||
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledTwice;
|
||||
});
|
||||
|
||||
it('stops retrying when a successful update has occurred', async () => {
|
||||
let updateStub = sandbox.stub(User, 'update');
|
||||
updateStub.onCall(0).returns(failedMock);
|
||||
updateStub.returns(successfulMock);
|
||||
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledThrice;
|
||||
});
|
||||
|
||||
it('retries failed updates at most five times per user', async () => {
|
||||
sandbox.stub(User, 'update').returns(failedMock);
|
||||
|
||||
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
|
||||
|
||||
expect(User.update.callCount).to.eql(10);
|
||||
});
|
||||
});
|
||||
|
||||
it('gives out achievements', async () => {
|
||||
@@ -1171,13 +1273,15 @@ describe('Group Model', () => {
|
||||
|
||||
context('Party quests', () => {
|
||||
it('updates participating members with rewards', async () => {
|
||||
sandbox.spy(User, 'update');
|
||||
await party.finishQuest(quest);
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledTwice;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
_id: {
|
||||
$in: [questLeader._id, participatingMember._id],
|
||||
},
|
||||
_id: questLeader._id,
|
||||
});
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
_id: participatingMember._id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1204,6 +1308,7 @@ describe('Group Model', () => {
|
||||
});
|
||||
|
||||
it('updates all users with rewards', async () => {
|
||||
sandbox.spy(User, 'update');
|
||||
await party.finishQuest(tavernQuest);
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
|
||||
@@ -450,4 +450,84 @@ describe('Inventory Controller', function() {
|
||||
expect(scope.hasAllTimeTravelerItemsOfType('mounts')).to.eql(true);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('Gear search filter', function() {
|
||||
var wrap = function(text) {
|
||||
return {'text': function() {return text;}};
|
||||
}
|
||||
|
||||
var toText = function(list) {
|
||||
return _.map(list, function(ele) { return ele.text(); });
|
||||
}
|
||||
|
||||
var gearByClass, gearByType;
|
||||
|
||||
beforeEach(function() {
|
||||
scope.$digest();
|
||||
gearByClass = {'raw': [wrap('kale'), wrap('sashimi')],
|
||||
'cooked': [wrap('chicken'), wrap('potato')]};
|
||||
|
||||
gearByType = {'veg': [wrap('kale'), wrap('potato')],
|
||||
'not': [wrap('chicken'), wrap('sashimi')]};
|
||||
scope.gearByClass = gearByClass;
|
||||
scope.gearByType = gearByType;
|
||||
scope.equipmentQuery.query = 'a';
|
||||
});
|
||||
|
||||
it('filters nothing if equipmentQuery is nothing', function() {
|
||||
scope.equipmentQuery.query = '';
|
||||
scope.$digest();
|
||||
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
|
||||
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken', 'potato']);
|
||||
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
|
||||
expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken', 'sashimi']);
|
||||
});
|
||||
|
||||
it('filters out gear if class gear changes', function() {
|
||||
scope.$digest();
|
||||
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
|
||||
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato']);
|
||||
|
||||
scope.gearByClass['raw'].push(wrap('zucchini'));
|
||||
scope.gearByClass['cooked'].push(wrap('pizza'));
|
||||
scope.$digest();
|
||||
expect(toText(scope.filteredGearByClass['raw'])).to.eql(['kale', 'sashimi']);
|
||||
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['potato', 'pizza']);
|
||||
});
|
||||
|
||||
it('filters out gear if typed gear changes', function() {
|
||||
scope.$digest();
|
||||
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
|
||||
expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi']);
|
||||
|
||||
scope.gearByType['veg'].push(wrap('zucchini'));
|
||||
scope.gearByType['not'].push(wrap('pizza'));
|
||||
|
||||
scope.$digest();
|
||||
expect(toText(scope.filteredGearByType['veg'])).to.eql(['kale', 'potato']);
|
||||
expect(toText(scope.filteredGearByType['not'])).to.eql(['sashimi', 'pizza']);
|
||||
});
|
||||
|
||||
it('filters out gear if filter query changes', function() {
|
||||
scope.equipmentQuery.query = 'c';
|
||||
scope.$digest();
|
||||
|
||||
expect(toText(scope.filteredGearByClass['raw'])).to.eql([]);
|
||||
expect(toText(scope.filteredGearByClass['cooked'])).to.eql(['chicken']);
|
||||
expect(toText(scope.filteredGearByType['veg'])).to.eql([]);
|
||||
expect(toText(scope.filteredGearByType['not'])).to.eql(['chicken']);
|
||||
});
|
||||
|
||||
it('returns the right filtered gear', function() {
|
||||
var equipment = [wrap('spicy tuna'), wrap('dragon'), wrap('rainbow'), wrap('caterpillar')];
|
||||
expect(toText(scope.equipmentSearch(equipment, 'ra'))).to.eql(['dragon', 'rainbow']);
|
||||
});
|
||||
|
||||
it('returns the right filtered gear if the source gear has unicode', function() {
|
||||
// blue hat, red hat, red shield
|
||||
var equipment = [wrap('藍色軟帽'), wrap('紅色軟帽'), wrap('紅色盾牌')];
|
||||
// searching for 'red' gives red hat, red shield
|
||||
expect(toText(scope.equipmentSearch(equipment, '紅色'))).to.eql(['紅色軟帽', '紅色盾牌']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["transform-object-rest-spread"],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"syntax-async-functions",
|
||||
"transform-regenerator",
|
||||
],
|
||||
"comments": false
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { userGems } from 'client/store/getters';
|
||||
import { gems as userGems } from 'client/store/getters/user';
|
||||
|
||||
describe('userGems getter', () => {
|
||||
it('returns the user\'s gems', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import storeInjector from 'inject?-vue!client/store';
|
||||
import { mapState, mapGetters, mapActions } from 'client/store';
|
||||
import { flattenAndNamespace } from 'client/store/helpers/internals';
|
||||
|
||||
describe('Store', () => {
|
||||
let injectedStore;
|
||||
@@ -14,11 +15,25 @@ describe('Store', () => {
|
||||
computedName ({ state }) {
|
||||
return `${state.name} computed!`;
|
||||
},
|
||||
...flattenAndNamespace({
|
||||
nested: {
|
||||
computedName ({ state }) {
|
||||
return `${state.name} computed!`;
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
'./actions': {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
...flattenAndNamespace({
|
||||
nested: {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}).default;
|
||||
});
|
||||
@@ -41,17 +56,29 @@ describe('Store', () => {
|
||||
injectedStore.state.name = 'test updated';
|
||||
});
|
||||
|
||||
it('supports getters', () => {
|
||||
expect(injectedStore.getters.computedName).to.equal('test computed!');
|
||||
injectedStore.state.name = 'test updated';
|
||||
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
|
||||
describe('getters', () => {
|
||||
it('supports getters', () => {
|
||||
expect(injectedStore.getters.computedName).to.equal('test computed!');
|
||||
injectedStore.state.name = 'test updated';
|
||||
expect(injectedStore.getters.computedName).to.equal('test updated computed!');
|
||||
});
|
||||
|
||||
it('supports nested getters', () => {
|
||||
expect(injectedStore.getters['nested:computedName']).to.equal('test computed!');
|
||||
injectedStore.state.name = 'test updated';
|
||||
expect(injectedStore.getters['nested:computedName']).to.equal('test updated computed!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('can be dispatched', () => {
|
||||
it('can dispatch an action', () => {
|
||||
expect(injectedStore.dispatch('getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('can dispatch a nested action', () => {
|
||||
expect(injectedStore.dispatch('nested:getName', 1, 2, 3)).to.deep.equal(['test', 1, 2, 3]);
|
||||
});
|
||||
|
||||
it('throws an error is the action doesn\'t exists', () => {
|
||||
expect(() => injectedStore.dispatched('wrong')).to.throw;
|
||||
});
|
||||
@@ -116,5 +143,26 @@ describe('Store', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('flattenAndNamespace', () => {
|
||||
let result = flattenAndNamespace({
|
||||
nested: {
|
||||
computed ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
nested2: {
|
||||
getName ({ state }, ...args) {
|
||||
return [state.name, ...args];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(Object.keys(result).length).to.equal(3);
|
||||
expect(Object.keys(result).sort()).to.deep.equal(['nested2:getName', 'nested:computed', 'nested:getName']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
472
website/assets/sprites/dist/spritesmith-main-5.css
vendored
BIN
website/assets/sprites/dist/spritesmith-main-5.png
vendored
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 18 KiB |
@@ -91,6 +91,10 @@
|
||||
height: 10.5em
|
||||
width: 100%
|
||||
|
||||
// Covers avatars, health bar at 1005-768. Fix:
|
||||
@media (max-width: 1005px) and (min-width: 768px)
|
||||
margin-top: 2.8em;
|
||||
|
||||
// this is a wrapper for avatars in the header
|
||||
// inside this is the actual `herobox` module
|
||||
// that can be used anywhere on the site
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
vertical-align: bottom
|
||||
margin-right: 20px
|
||||
|
||||
.equipment-search
|
||||
max-width: 300px
|
||||
margin-right: 20px
|
||||
|
||||
.well.use-costume-info
|
||||
margin-top: 10px
|
||||
p:first-child
|
||||
@@ -67,6 +71,7 @@ menu.pets div
|
||||
z-index: 1
|
||||
padding-bottom: 0
|
||||
|
||||
|
||||
.inventory-list
|
||||
p
|
||||
//display: none
|
||||
|
||||
@@ -16,6 +16,11 @@
|
||||
@extend $hrpg-button
|
||||
hrpg-button-color-mixin($color-options-menu)
|
||||
|
||||
&.highlight
|
||||
hrpg-button-color-mixin($color-options-submenu, true)
|
||||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
|
||||
.options-submenu
|
||||
margin-top: -1.618em
|
||||
background-color: $color-options-submenu
|
||||
@@ -33,5 +38,3 @@
|
||||
padding: 1em 0.618em 0 0.618em
|
||||
border: 1px solid darken($color-options-menu, 12%)
|
||||
border-top: none
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ habitrpg.controller("InventoryCtrl",
|
||||
|
||||
$scope.selectedEgg = null; // {index: 1, name: "Tiger", value: 5}
|
||||
$scope.selectedPotion = null; // {index: 5, name: "Red", value: 3}
|
||||
$scope.equipmentQuery = {'query': ''};
|
||||
|
||||
_updateDropAnimalCount(user.items);
|
||||
|
||||
@@ -85,6 +86,50 @@ habitrpg.controller("InventoryCtrl",
|
||||
})
|
||||
}, true);
|
||||
|
||||
$scope.equipmentSearch = function(equipment, term) {
|
||||
if (!equipment) return;
|
||||
if (!angular.isString(term) || term.length == 0) {
|
||||
return equipment;
|
||||
}
|
||||
termMatcher = new RegExp(term, 'i');
|
||||
|
||||
var result = [];
|
||||
for (var i = 0; i < equipment.length; i++) {
|
||||
if (termMatcher.test(equipment[i].text())) {
|
||||
result.push(equipment[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
$scope.updateEquipment = function(gearByClass, gearByType, equipmentQuery) {
|
||||
$scope.filteredGearByClass = {};
|
||||
$scope.filteredGearByType = {};
|
||||
_.forEach(gearByClass, function(value, key) {
|
||||
var searchResult = $scope.equipmentSearch(value, equipmentQuery);
|
||||
if (searchResult.length > 0) {
|
||||
$scope.filteredGearByClass[key] = searchResult;
|
||||
}
|
||||
});
|
||||
_.forEach(gearByType, function(value, key) {
|
||||
var searchResult = $scope.equipmentSearch(value, equipmentQuery);
|
||||
if (searchResult.length > 0) {
|
||||
$scope.filteredGearByType[key] = searchResult;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$watch(function(){
|
||||
return ['gearByClass', 'gearByType', 'equipmentQuery'].map(angular.bind($scope, $scope.$eval));
|
||||
}, function(updatedVals) {
|
||||
var gearByClass = updatedVals[0];
|
||||
var gearByType = updatedVals[1];
|
||||
var equipmentQuery = updatedVals[2];
|
||||
$scope.updateEquipment(gearByClass, gearByType, equipmentQuery.query);
|
||||
}, true);
|
||||
|
||||
$scope.updateEquipment($scope.gearByClass, $scope.gearByType, $scope.equipmentQuery.query);
|
||||
|
||||
$scope.chooseEgg = function(egg){
|
||||
if ($scope.selectedEgg && $scope.selectedEgg.key == egg) {
|
||||
return $scope.selectedEgg = null; // clicked same egg, unselect
|
||||
|
||||
@@ -18,6 +18,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
||||
$timeout(function () {
|
||||
if (window.env.IS_MOBILE || User.user.preferences.stickyHeader === false) return;
|
||||
$('.header-wrap').sticky({topSpacing:0});
|
||||
$(window).resize(function() {$('.header-wrap').sticky('update');});
|
||||
});
|
||||
|
||||
// Remove listener
|
||||
|
||||
@@ -119,6 +119,9 @@ angular.module('habitrpg')
|
||||
Groups.data.party = party;
|
||||
$state.go('options.social.party');
|
||||
resolve();
|
||||
if ($state.current.name === "options.social.party") {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
BIN
website/client-old/merch/teespring-mug-eu-logo.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
website/client-old/merch/teespring-mug-eu.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
website/client-old/merch/teespring-mug-logo.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
website/client-old/merch/teespring-mug.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"plugins": ["transform-object-rest-spread"],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"syntax-async-functions",
|
||||
"transform-regenerator",
|
||||
],
|
||||
}
|
||||
@@ -213,7 +213,9 @@ import { mapState, mapGetters } from '../store';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters(['userGems']),
|
||||
...mapGetters({
|
||||
userGems: 'user:gems',
|
||||
}),
|
||||
...mapState(['user']),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,8 +44,8 @@ let userDataWatcher = store.watch(state => [state.user, state.tasks], ([user, ta
|
||||
|
||||
// Load the user and the user tasks
|
||||
Promise.all([
|
||||
store.dispatch('user.fetch'),
|
||||
store.dispatch('tasks.fetchUserTasks'),
|
||||
store.dispatch('user:fetch'),
|
||||
store.dispatch('tasks:fetchUserTasks'),
|
||||
]).catch(() => {
|
||||
alert('Impossible to fetch user. Copy into localStorage a valid habit-mobile-settings object.');
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import tasks from './tasks';
|
||||
import user from './user';
|
||||
import { flattenAndNamespace } from '../helpers/internals';
|
||||
import * as tasks from './tasks';
|
||||
import * as user from './user';
|
||||
|
||||
const actions = {
|
||||
tasks,
|
||||
// Actions should be named as 'actionName' and can be accessed as 'namespace.actionName'
|
||||
// Example: fetch in user.js -> 'user.fetch'
|
||||
|
||||
const actions = flattenAndNamespace({
|
||||
user,
|
||||
};
|
||||
tasks,
|
||||
});
|
||||
|
||||
export default actions;
|
||||
@@ -1,15 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
const actions = {};
|
||||
|
||||
actions.fetchUserTasks = function fetchUserTasks (store) {
|
||||
let promise = Vue.http.get('/api/v3/tasks/user');
|
||||
|
||||
promise.then((response) => {
|
||||
store.state.tasks = response.body.data;
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
export default actions;
|
||||
export async function fetchUserTasks (store) {
|
||||
let response = await Vue.http.get('/api/v3/tasks/user');
|
||||
store.state.tasks = response.body.data;
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
const actions = {};
|
||||
|
||||
actions.fetch = function fetchUser (store) {
|
||||
let promise = Vue.http.get('/api/v3/user');
|
||||
|
||||
promise.then((response) => {
|
||||
store.state.user = response.body.data;
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
export default actions;
|
||||
export async function fetch (store) { // eslint-disable-line no-shadow
|
||||
let response = await Vue.http.get('/api/v3/user');
|
||||
store.state.user = response.body.data;
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
export function userGems (store) {
|
||||
return store.state.user.balance * 4;
|
||||
}
|
||||
import { flattenAndNamespace } from '../helpers/internals';
|
||||
import * as user from './user';
|
||||
|
||||
// Getters should be named as 'getterName' and can be accessed as 'namespace.getterName'
|
||||
// Example: gems in user.js -> 'user.gems'
|
||||
|
||||
const getters = flattenAndNamespace({
|
||||
user,
|
||||
});
|
||||
|
||||
export default getters;
|
||||
3
website/client/store/getters/user.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function gems (store) {
|
||||
return store.state.user.balance * 4;
|
||||
}
|
||||
32
website/client/store/helpers/internals.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/* Flatten multiple objects into a single, namespaced object.
|
||||
|
||||
Example:
|
||||
|
||||
getters
|
||||
user
|
||||
gems
|
||||
tasks
|
||||
...
|
||||
tasks
|
||||
todos
|
||||
dailys
|
||||
...
|
||||
|
||||
Result:
|
||||
getters
|
||||
user.gems
|
||||
user.tasks
|
||||
tasks.todos
|
||||
tasks.dailys
|
||||
*/
|
||||
export function flattenAndNamespace (namespaces) {
|
||||
let result = {};
|
||||
|
||||
Object.keys(namespaces).forEach(namespace => {
|
||||
Object.keys(namespaces[namespace]).forEach(itemName => {
|
||||
result[`${namespace}:${itemName}`] = namespaces[namespace][itemName];
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import Vue from 'vue';
|
||||
import state from './state';
|
||||
import actions from './actions';
|
||||
import * as getters from './getters';
|
||||
import { get } from 'lodash';
|
||||
import getters from './getters';
|
||||
|
||||
// Central application store for Habitica
|
||||
// Heavily inspired to Vuex (https://github.com/vuejs/vuex) with a very
|
||||
@@ -22,7 +21,7 @@ const store = {
|
||||
// Actions should be called using store.dispatch(ACTION_NAME, ...ARGS)
|
||||
// They get passed the store instance and any additional argument passed to dispatch()
|
||||
dispatch (type, ...args) {
|
||||
let action = get(actions, type);
|
||||
let action = actions[type];
|
||||
|
||||
if (!action) throw new Error(`Action "${type}" not found.`);
|
||||
return action(store, ...args);
|
||||
@@ -60,7 +59,7 @@ export {
|
||||
mapState,
|
||||
mapGetters,
|
||||
mapActions,
|
||||
} from './helpers';
|
||||
} from './helpers/public';
|
||||
|
||||
// Setup internal Vue instance to make state and getters reactive
|
||||
_vm = new Vue({
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
{
|
||||
"merch" : "Merchandise",
|
||||
"merchandiseDescription": "Looking for t-shirts, mugs, or stickers to show off your Habitica pride? Click here!",
|
||||
|
||||
"merch-teespring-summary" : "Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
|
||||
"merch-teespring-goto" : "Get a Habitica T-shirt",
|
||||
|
||||
"merch-teespring-mug-summary" : "Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
|
||||
"merch-teespring-mug-goto" : "Get a Habitica Mug",
|
||||
|
||||
"merch-teespring-eu-summary" : "EUROPEAN VERSION : Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
|
||||
"merch-teespring-eu-goto" : "Get a Habitica T-shirt (EU)",
|
||||
|
||||
"merch-teespring-mug-eu-summary" : "EUROPEAN VERSION : Teespring is a platform that makes it easy for anyone to create and sell high-quality products people love, with no cost or risk.",
|
||||
"merch-teespring-mug-eu-goto" : "Get a Habitica Mug (EU)",
|
||||
|
||||
"merch-stickermule-summary" : "Stick proud Melior wherever you (or someone else) need a reminder of both present and future accomplishments!",
|
||||
"merch-stickermule-goto" : "Get Habitica stickers"
|
||||
|
||||
|
||||
@@ -92,5 +92,7 @@
|
||||
"petsReleased": "Pets released.",
|
||||
"mountsAndPetsReleased": "Mounts and pets released",
|
||||
"mountsReleased": "Mounts released",
|
||||
"gemsEach": "gems each"
|
||||
"gemsEach": "gems each",
|
||||
"foodWikiText": "What does my pet like to eat?",
|
||||
"foodWikiUrl": "http://habitica.wikia.com/wiki/Food_Preferences"
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ api.createSubscription = async function createSubscription (data) {
|
||||
plan.dateTerminated = moment(plan.dateTerminated).add({months}).toDate();
|
||||
} else {
|
||||
plan.dateTerminated = moment().add({months}).toDate();
|
||||
plan.dateCreated = today;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ const LARGE_GROUP_COUNT_MESSAGE_CUTOFF = shared.constants.LARGE_GROUP_COUNT_MESS
|
||||
|
||||
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
||||
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
||||
const MAX_UPDATE_RETRIES = 5;
|
||||
|
||||
export let schema = new Schema({
|
||||
name: {type: String, required: true},
|
||||
@@ -577,6 +578,19 @@ schema.statics.cleanGroupQuest = function cleanGroupQuest () {
|
||||
};
|
||||
};
|
||||
|
||||
async function _updateUserWithRetries (userId, updates, numTry = 1) {
|
||||
return await User.update({_id: userId}, updates).exec()
|
||||
.then((raw) => {
|
||||
return raw;
|
||||
}).catch((err) => {
|
||||
if (numTry < MAX_UPDATE_RETRIES) {
|
||||
return _updateUserWithRetries(userId, updates, ++numTry);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Participants: Grant rewards & achievements, finish quest.
|
||||
// Changes the group object update members
|
||||
schema.methods.finishQuest = async function finishQuest (quest) {
|
||||
@@ -623,11 +637,19 @@ schema.methods.finishQuest = async function finishQuest (quest) {
|
||||
}
|
||||
});
|
||||
|
||||
let q = this._id === TAVERN_ID ? {} : {_id: {$in: this.getParticipatingQuestMembers()}};
|
||||
let participants = this._id === TAVERN_ID ? {} : this.getParticipatingQuestMembers();
|
||||
this.quest = {};
|
||||
this.markModified('quest');
|
||||
|
||||
return await User.update(q, updates, {multi: true}).exec();
|
||||
if (this._id === TAVERN_ID) {
|
||||
return await User.update({}, updates, {multi: true}).exec();
|
||||
}
|
||||
|
||||
let promises = participants.map(userId => {
|
||||
return _updateUserWithRetries(userId, updates);
|
||||
});
|
||||
|
||||
return Bluebird.all(promises);
|
||||
};
|
||||
|
||||
function _isOnQuest (user, progress, group) {
|
||||
@@ -835,9 +857,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
|
||||
|
||||
schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
||||
let group = this;
|
||||
let update = {
|
||||
$inc: {memberCount: -1},
|
||||
};
|
||||
let update = {};
|
||||
|
||||
let challenges = await Challenge.find({
|
||||
_id: {$in: user.challenges},
|
||||
@@ -867,18 +887,30 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') {
|
||||
|
||||
// If user is the last one in group and group is private, delete it
|
||||
if (group.memberCount <= 1 && group.privacy === 'private') {
|
||||
promises.push(group.remove());
|
||||
} else { // otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
|
||||
if (group.leader === user._id) {
|
||||
let query = group.type === 'party' ? {'party._id': group._id} : {guilds: group._id};
|
||||
query._id = {$ne: user._id};
|
||||
let seniorMember = await User.findOne(query).select('_id').exec();
|
||||
|
||||
// could be missing in case of public guild (that can have 0 members) with 1 member who is leaving
|
||||
if (seniorMember) update.$set = {leader: seniorMember._id};
|
||||
// double check the member count is correct so we don't accidentally delete a group that still has users in it
|
||||
let members;
|
||||
if (group.type === 'guild') {
|
||||
members = await User.find({guilds: group._id}).select('_id').exec();
|
||||
} else {
|
||||
members = await User.find({'party._id': group._id}).select('_id').exec();
|
||||
}
|
||||
_.remove(members, {_id: user._id});
|
||||
if (members.length === 0) {
|
||||
promises.push(group.remove());
|
||||
return await Bluebird.all(promises);
|
||||
}
|
||||
promises.push(group.update(update).exec());
|
||||
}
|
||||
// otherwise If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
|
||||
update.$inc = {memberCount: -1};
|
||||
if (group.leader === user._id) {
|
||||
let query = group.type === 'party' ? {'party._id': group._id} : {guilds: group._id};
|
||||
query._id = {$ne: user._id};
|
||||
let seniorMember = await User.findOne(query).select('_id').exec();
|
||||
|
||||
// could be missing in case of public guild (that can have 0 members) with 1 member who is leaving
|
||||
if (seniorMember) update.$set = {leader: seniorMember._id};
|
||||
}
|
||||
promises.push(group.update(update).exec());
|
||||
|
||||
return await Bluebird.all(promises);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
@@ -31,16 +31,22 @@ mixin equipmentButton(type)
|
||||
popover-append-to-body='true')
|
||||
|
||||
mixin equipmentList(equipmentType)
|
||||
menu.pets-menu(label='{{::label}}', ng-show='gearByClass[klass]', ng-if='groupingChoice === "klass"',
|
||||
menu.pets-menu(label='{{::label}}', ng-show='filteredGearByClass[klass]', ng-if='groupingChoice === "klass"',
|
||||
ng-repeat='(klass,label) in {warrior:env.t("warrior"), wizard:env.t("mage"), rogue:env.t("rogue"), healer:env.t("healer"), special:env.t("special"), mystery:env.t("mystery"), armoire:env.t("armoireText")}')
|
||||
div(ng-repeat='item in gearByClass[klass] | orderBy: order')
|
||||
div(ng-repeat='item in filteredGearByClass[klass] | orderBy: order')
|
||||
+equipmentButton(equipmentType)
|
||||
menu.pets-menu(label='{{::label}}', ng-show='gearByType[type]', ng-if='groupingChoice === "equipmentType"',
|
||||
menu.pets-menu(label='{{::label}}', ng-show='filteredGearByType[type]', ng-if='groupingChoice === "equipmentType"',
|
||||
ng-repeat='(type,label) in {headAccessory:env.t("headAccessoryCapitalized"), head:env.t("headgearCapitalized"), eyewear:env.t("eyewear"), weapon:env.t("weaponCapitalized"), shield:env.t("offhandCapitalized"), armor:env.t("armorCapitalized"), body:env.t("body"), back:env.t("back")}')
|
||||
div(ng-repeat='item in gearByType[type] | orderBy: order', ng-show='item.klass !== "base"')
|
||||
div(ng-repeat='item in filteredGearByType[type] | orderBy: order', ng-show='item.klass !== "base"')
|
||||
+equipmentButton(equipmentType)
|
||||
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-6
|
||||
.input-group.equipment-search
|
||||
.input-group-addon
|
||||
.glyphicon.glyphicon-search
|
||||
input.form-control(type='text', placeholder=env.t('search'), ng-model='equipmentQuery.query', ng-model-options='{ debounce: 250 }')
|
||||
.row
|
||||
.col-md-6.border-right(ng-controller="SortableInventoryController")
|
||||
h3.equipment-title.hint(popover-trigger='mouseenter',
|
||||
|
||||
@@ -20,6 +20,8 @@ ul.options-menu
|
||||
li(ng-class="{ active: $state.includes('options.inventory.seasonalshop') }")
|
||||
a(ui-sref='options.inventory.seasonalshop')
|
||||
=env.t('seasonalShop')
|
||||
li.pull-right.highlight(tooltip=env.t('merchandiseDescription'))
|
||||
a(href='/static/merch')=env.t('merch')
|
||||
|
||||
.tab-content
|
||||
.tab-pane.active
|
||||
|
||||
@@ -62,6 +62,7 @@ mixin petList(eggSource, potionSource)
|
||||
menu.inventory-list(type='list', ng-if='foodCount > 0')
|
||||
li.customize-menu
|
||||
menu.pets-menu(label=env.t('food'))
|
||||
a(href=env.t('foodWikiUrl'), target='_blank')=env.t('foodWikiText')
|
||||
div(ng-repeat='(food,points) in ownedItems(user.items.food)')
|
||||
button.customize-option(popover-append-to-body='true', popover='{{:: Content.food[food].notes()}}', popover-title='{{:: Content.food[food].text()}}', popover-trigger='mouseenter', popover-placement='top', ng-click='chooseFood(food)', ng-class='{selectableInventory: selectedFood == Content.food[food]}', class='Pet_Food_{{::food}}')
|
||||
.badge.badge-info.stack-count {{points}}
|
||||
|
||||
@@ -1,42 +1,55 @@
|
||||
h2 12/8/2016 - GIFT-1-GET-1 SUBSCRIPTIONS, NEW PET QUEST, AND HOLIDAY PREP TIPS
|
||||
h2 12/12/2016 - LAST CHANCE FOR HABITICA T-SHIRTS AND MUGS; STAFF SPOTLIGHT
|
||||
hr
|
||||
tr
|
||||
td
|
||||
.inventory_present_01.pull-left.slight-right-margin
|
||||
h3 Gift a Subscription and Get One Free!
|
||||
p In honor of the season of giving, we're running a special promotion for the next month only. Now when you gift somebody <a href='/#/options/settings/subscription'>a subscription</a>, you get the same subscription for yourself for free!
|
||||
br
|
||||
p Subscribers get tons of perks every month, including exclusive items, the ability to buy gems with gold, and increased data history. Plus, it helps keep Habitica running :) To gift a subscription to someone, just open their profile and click on the present icon in the lower-left.
|
||||
br
|
||||
p The special promotion will only run until January 6th, so if you've been curious about trying out a subscription, now's the time! Make a friend happy and use all your new gems to go questing together.
|
||||
p.small.muted by SabreCat and Lemoness
|
||||
.promo_coffee_mug.pull-left.slight-right-margin
|
||||
h3 Last Chance for Habitica T-Shirts and Mugs!
|
||||
p Today's the final day to get <a href='https://teespring.com/stores/habitica' target='_blank'>our Habitica T-shirts and mugs</a> if you live in the EU, and tomorrow's the final day if you live in the USA, so if you want one, be sure to grab it now! Thanks again for all your support -- you're all awesome.
|
||||
.promo_contrib_spotlight_Keith.pull-right
|
||||
p.small.muted by Redphoenix and Beffymaroo
|
||||
tr
|
||||
td
|
||||
span.Mount_Body_Sloth-Base.pull-right
|
||||
span.Mount_Head_Sloth-Base.pull-right(style='margin:0')
|
||||
h3 New Pet Quest: Sloth!
|
||||
p The Somnolent Sloth is making everyone sleepy! Can you shake off the snow and get stuff done? Get the latest pet quest, <a href='/#/options/inventory/quests'>The Somnolent Sloth</a>, and earn some speedy sloth pets by completing your real-life tasks.
|
||||
p.small.muted Written by PixelHunter
|
||||
p.small.muted Art by JaizakAripaik, Drevian, McCoyly, awakebyjava, PainterProphet, Kiwibot, and greenpencil
|
||||
tr
|
||||
td
|
||||
.promo_winter_fireworks.pull-left.slight-right-margin
|
||||
h3 Guild Spotlight: Preparing for the Holidays
|
||||
p There's a new <a href='https://habitica.wordpress.com/2016/11/29/prepping-for-the-holidays/' target='_blank'>Guild Spotlight on the blog</a> that highlights the Guilds that can help you prepare for the holidays! Check it out now to find Habitican communities that can keep you stress-free as seasonal celebrations approach.
|
||||
p.small.muted by Beffymaroo
|
||||
tr
|
||||
td
|
||||
h3 Use Case Spotlight: Holiday Survival Tips
|
||||
p This month's <a href='https://habitica.wordpress.com/2016/12/06/use-case-spotlight-prepping-for-the-holidays/' target='_blank'>Use Case Spotlight</a> is about ways to use Habitica to make the holidays more enjoyable! It features a number of great suggestions submitted by Habiticans in the <a href='/#/options/groups/guilds/1d3a10bf-60aa-4806-a38b-82d1084a59e6'>Use Case Spotlights Guild</a>. We hope it helps you!
|
||||
br
|
||||
p Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to manage your health and fitness? We’ll 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!
|
||||
p.small.muted by Beffymaroo
|
||||
h3 Staff Spotlight: Keith
|
||||
p We've posted a new <a href='https://habitica.wordpress.com/2016/12/09/staff-spotlight-keith-aka-thehollidayinn/' target='_blank'>Staff Spotlight</a> on the blog! Check out our latest interview with Keith AKA TheHollidayInn, our newest staff member and awesome engineer.
|
||||
p.small.muted by Lemoness, Beffymaroo, and TheHollidayInn
|
||||
|
||||
if menuItem !== 'oldNews'
|
||||
hr
|
||||
a(href='/static/old-news', target='_blank') Read older news
|
||||
|
||||
mixin oldNews
|
||||
h2 12/8/2016 - GIFT-1-GET-1 SUBSCRIPTIONS, NEW PET QUEST, AND HOLIDAY PREP TIPS
|
||||
tr
|
||||
td
|
||||
.inventory_present_01.pull-left.slight-right-margin
|
||||
h3 Gift a Subscription and Get One Free!
|
||||
p In honor of the season of giving, we're running a special promotion for the next month only. Now when you gift somebody <a href='/#/options/settings/subscription'>a subscription</a>, you get the same subscription for yourself for free!
|
||||
br
|
||||
p Subscribers get tons of perks every month, including exclusive items, the ability to buy gems with gold, and increased data history. Plus, it helps keep Habitica running :) To gift a subscription to someone, just open their profile and click on the present icon in the lower-left.
|
||||
br
|
||||
p The special promotion will only run until January 6th, so if you've been curious about trying out a subscription, now's the time! Make a friend happy and use all your new gems to go questing together.
|
||||
p.small.muted by SabreCat and Lemoness
|
||||
tr
|
||||
td
|
||||
span.Mount_Body_Sloth-Base.pull-right
|
||||
span.Mount_Head_Sloth-Base.pull-right(style='margin:0')
|
||||
h3 New Pet Quest: Sloth!
|
||||
p The Somnolent Sloth is making everyone sleepy! Can you shake off the snow and get stuff done? Get the latest pet quest, <a href='/#/options/inventory/quests'>The Somnolent Sloth</a>, and earn some speedy sloth pets by completing your real-life tasks.
|
||||
p.small.muted Written by PixelHunter
|
||||
p.small.muted Art by JaizakAripaik, Drevian, McCoyly, awakebyjava, PainterProphet, Kiwibot, and greenpencil
|
||||
tr
|
||||
td
|
||||
.promo_winter_fireworks.pull-left.slight-right-margin
|
||||
h3 Guild Spotlight: Preparing for the Holidays
|
||||
p There's a new <a href='https://habitica.wordpress.com/2016/11/29/prepping-for-the-holidays/' target='_blank'>Guild Spotlight on the blog</a> that highlights the Guilds that can help you prepare for the holidays! Check it out now to find Habitican communities that can keep you stress-free as seasonal celebrations approach.
|
||||
p.small.muted by Beffymaroo
|
||||
tr
|
||||
td
|
||||
h3 Use Case Spotlight: Holiday Survival Tips
|
||||
p This month's <a href='https://habitica.wordpress.com/2016/12/06/use-case-spotlight-prepping-for-the-holidays/' target='_blank'>Use Case Spotlight</a> is about ways to use Habitica to make the holidays more enjoyable! It features a number of great suggestions submitted by Habiticans in the <a href='/#/options/groups/guilds/1d3a10bf-60aa-4806-a38b-82d1084a59e6'>Use Case Spotlights Guild</a>. We hope it helps you!
|
||||
br
|
||||
p Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to manage your health and fitness? We’ll 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!
|
||||
p.small.muted by Beffymaroo
|
||||
h2 12/1/2016 - NEW BACKGROUNDS AND ARMOIRE ITEMS, iOS UPDATE, TAKE THIS CHALLENGE, AND HABITICA STAFF MEMBER
|
||||
.promo_backgrounds_armoire_201612.pull-right
|
||||
tr
|
||||
|
||||
@@ -19,6 +19,8 @@ block vars
|
||||
- var merchants = []
|
||||
- merchants.push({key: 'teespring', logo: true, name: 'Teespring', link: 'https://teespring.com/habitica-gryphon-t-shirt'})
|
||||
- merchants.push({key: 'teespring-eu', logo: true, name: 'Teespring (EU)', link: 'https://teespring.com/habitica-gryphon-t-shirt_eu'})
|
||||
- merchants.push({key: 'teespring-mug', logo: true, name: 'Teespring', link: 'https://teespring.com/shop/habitica-gryphon-mug'})
|
||||
- merchants.push({key: 'teespring-mug-eu', logo: true, name: 'Teespring (EU)', link: 'https://teespring.com/shop/habitica-hydration-mug-eu'})
|
||||
- merchants.push({key: 'stickermule', logo: true, name: 'Stickermule', link: 'https://www.stickermule.com/uk/marketplace/9317-habitica-gryphon-sticker'})
|
||||
|
||||
block title
|
||||
|
||||