mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Merge branch 'release' into develop
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "habitica",
|
"name": "habitica",
|
||||||
"version": "4.92.5",
|
"version": "4.92.6",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "habitica",
|
"name": "habitica",
|
||||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||||
"version": "4.92.5",
|
"version": "4.92.6",
|
||||||
"main": "./website/server/index.js",
|
"main": "./website/server/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/trace-agent": "^3.6.0",
|
"@google-cloud/trace-agent": "^3.6.0",
|
||||||
|
|||||||
@@ -142,4 +142,52 @@ describe('shared.ops.buy', () => {
|
|||||||
buy(user, {params: {key: 'potion'}, quantity: 2});
|
buy(user, {params: {key: 'potion'}, quantity: 2});
|
||||||
expect(user.stats.hp).to.eql(50);
|
expect(user.stats.hp).to.eql(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('errors if user supplies a non-numeric quantity', (done) => {
|
||||||
|
try {
|
||||||
|
buy(user, {
|
||||||
|
params: {
|
||||||
|
key: 'dilatoryDistress1',
|
||||||
|
},
|
||||||
|
type: 'quest',
|
||||||
|
quantity: 'bogle',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(errorMessage('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if user supplies a negative quantity', (done) => {
|
||||||
|
try {
|
||||||
|
buy(user, {
|
||||||
|
params: {
|
||||||
|
key: 'dilatoryDistress1',
|
||||||
|
},
|
||||||
|
type: 'quest',
|
||||||
|
quantity: -3,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(errorMessage('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if user supplies a decimal quantity', (done) => {
|
||||||
|
try {
|
||||||
|
buy(user, {
|
||||||
|
params: {
|
||||||
|
key: 'dilatoryDistress1',
|
||||||
|
},
|
||||||
|
type: 'quest',
|
||||||
|
quantity: 1.83,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(errorMessage('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,6 +108,47 @@ describe('shared.ops.purchase', () => {
|
|||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a non-numeric quantity', (done) => {
|
||||||
|
let type = 'eggs';
|
||||||
|
let key = 'Wolf';
|
||||||
|
|
||||||
|
try {
|
||||||
|
purchase(user, {params: {type, key}, quantity: 'jamboree'}, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a negative quantity', (done) => {
|
||||||
|
let type = 'eggs';
|
||||||
|
let key = 'Wolf';
|
||||||
|
user.balance = 10;
|
||||||
|
|
||||||
|
try {
|
||||||
|
purchase(user, {params: {type, key}, quantity: -2}, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a decimal quantity', (done) => {
|
||||||
|
let type = 'eggs';
|
||||||
|
let key = 'Wolf';
|
||||||
|
user.balance = 10;
|
||||||
|
|
||||||
|
try {
|
||||||
|
purchase(user, {params: {type, key}, quantity: 2.9}, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('successful purchase', () => {
|
context('successful purchase', () => {
|
||||||
|
|||||||
@@ -1,66 +1,72 @@
|
|||||||
.promo_april_fools_2019 {
|
.promo_april_fools_2019 {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: 0px -425px;
|
background-position: 0px -840px;
|
||||||
width: 423px;
|
width: 423px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_armoire_backgrounds_201904 {
|
.promo_armoire_backgrounds_201904 {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: 0px -573px;
|
background-position: -424px -840px;
|
||||||
width: 423px;
|
width: 423px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
|
.promo_butterflies {
|
||||||
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
|
background-position: 0px 0px;
|
||||||
|
width: 676px;
|
||||||
|
height: 676px;
|
||||||
|
}
|
||||||
.promo_celestial_rainbow_potions {
|
.promo_celestial_rainbow_potions {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: 0px -277px;
|
background-position: -433px -677px;
|
||||||
width: 423px;
|
width: 423px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_classes_spring2019 {
|
.promo_classes_spring2019 {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: -328px 0px;
|
background-position: 0px -677px;
|
||||||
width: 432px;
|
width: 432px;
|
||||||
height: 162px;
|
height: 162px;
|
||||||
}
|
}
|
||||||
.promo_egg_hunt {
|
.promo_egg_hunt {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: -761px 0px;
|
background-position: -1005px 0px;
|
||||||
width: 354px;
|
width: 354px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_mystery_201903 {
|
.promo_mystery_201903 {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: -761px -444px;
|
background-position: -1005px -444px;
|
||||||
width: 351px;
|
width: 351px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_seasonalshop_spring {
|
.promo_seasonalshop_spring {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: -424px -277px;
|
background-position: -1005px -592px;
|
||||||
width: 162px;
|
width: 162px;
|
||||||
height: 138px;
|
height: 138px;
|
||||||
}
|
}
|
||||||
.promo_shiny_seeds {
|
.promo_shiny_seeds {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: -761px -296px;
|
background-position: -1005px -296px;
|
||||||
width: 351px;
|
width: 351px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_spring_avatar_customizations {
|
.promo_spring_avatar_customizations {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: -761px -148px;
|
background-position: -1005px -148px;
|
||||||
width: 354px;
|
width: 354px;
|
||||||
height: 147px;
|
height: 147px;
|
||||||
}
|
}
|
||||||
.promo_take_this {
|
.promo_take_this {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: -761px -592px;
|
background-position: -1168px -592px;
|
||||||
width: 96px;
|
width: 96px;
|
||||||
height: 69px;
|
height: 69px;
|
||||||
}
|
}
|
||||||
.scene_yesterdailies_repeatables {
|
.scene_yesterdailies_repeatables {
|
||||||
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
background-position: 0px 0px;
|
background-position: -677px 0px;
|
||||||
width: 327px;
|
width: 327px;
|
||||||
height: 276px;
|
height: 276px;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 1.0 MiB |
@@ -48,7 +48,7 @@
|
|||||||
strong {{ $t('howManyToBuy') }}
|
strong {{ $t('howManyToBuy') }}
|
||||||
div(v-if='showAmountToBuy(item)')
|
div(v-if='showAmountToBuy(item)')
|
||||||
.box
|
.box
|
||||||
input(type='number', min='0', v-model.number='selectedAmountToBuy')
|
input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy')
|
||||||
span(:class="{'notEnough': notEnoughCurrency}")
|
span(:class="{'notEnough': notEnoughCurrency}")
|
||||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
|
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
|
||||||
span.cost(:class="getPriceClass()") {{ item.value }}
|
span.cost(:class="getPriceClass()") {{ item.value }}
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
button.btn.btn-primary(
|
button.btn.btn-primary(
|
||||||
@click="buyItem()",
|
@click="buyItem()",
|
||||||
v-else,
|
v-else,
|
||||||
:disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft',
|
:disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid',
|
||||||
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
||||||
) {{ $t('buyNow') }}
|
) {{ $t('buyNow') }}
|
||||||
|
|
||||||
@@ -260,6 +260,7 @@
|
|||||||
import * as Analytics from 'client/libs/analytics';
|
import * as Analytics from 'client/libs/analytics';
|
||||||
import spellsMixin from 'client/mixins/spells';
|
import spellsMixin from 'client/mixins/spells';
|
||||||
import planGemLimits from 'common/script/libs/planGemLimits';
|
import planGemLimits from 'common/script/libs/planGemLimits';
|
||||||
|
import numberInvalid from 'client/mixins/numberInvalid';
|
||||||
|
|
||||||
import svgClose from 'assets/svg/close.svg';
|
import svgClose from 'assets/svg/close.svg';
|
||||||
import svgGold from 'assets/svg/gold.svg';
|
import svgGold from 'assets/svg/gold.svg';
|
||||||
@@ -291,7 +292,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [currencyMixin, notifications, spellsMixin, buyMixin],
|
mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
|
||||||
components: {
|
components: {
|
||||||
BalanceInfo,
|
BalanceInfo,
|
||||||
EquipmentAttributesGrid,
|
EquipmentAttributesGrid,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
.how-many-to-buy
|
.how-many-to-buy
|
||||||
strong {{ $t('howManyToBuy') }}
|
strong {{ $t('howManyToBuy') }}
|
||||||
.box
|
.box
|
||||||
input(type='number', min='0', v-model.number='selectedAmountToBuy')
|
input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy')
|
||||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
|
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
|
||||||
span.value(:class="priceType") {{ item.value }}
|
span.value(:class="priceType") {{ item.value }}
|
||||||
|
|
||||||
@@ -34,7 +34,8 @@
|
|||||||
button.btn.btn-primary(
|
button.btn.btn-primary(
|
||||||
@click="buyItem()",
|
@click="buyItem()",
|
||||||
v-else,
|
v-else,
|
||||||
:class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}"
|
:class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}",
|
||||||
|
:disabled='numberInvalid',
|
||||||
) {{ $t('buyNow') }}
|
) {{ $t('buyNow') }}
|
||||||
|
|
||||||
div.right-sidebar(v-if="item.drop")
|
div.right-sidebar(v-if="item.drop")
|
||||||
@@ -207,12 +208,13 @@
|
|||||||
import QuestInfo from './questInfo.vue';
|
import QuestInfo from './questInfo.vue';
|
||||||
import notifications from 'client/mixins/notifications';
|
import notifications from 'client/mixins/notifications';
|
||||||
import buyMixin from 'client/mixins/buy';
|
import buyMixin from 'client/mixins/buy';
|
||||||
|
import numberInvalid from 'client/mixins/numberInvalid';
|
||||||
|
|
||||||
import questDialogDrops from './questDialogDrops';
|
import questDialogDrops from './questDialogDrops';
|
||||||
import questDialogContent from './questDialogContent';
|
import questDialogContent from './questDialogContent';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [currencyMixin, notifications, buyMixin],
|
mixins: [buyMixin, currencyMixin, notifications, numberInvalid],
|
||||||
components: {
|
components: {
|
||||||
BalanceInfo,
|
BalanceInfo,
|
||||||
QuestInfo,
|
QuestInfo,
|
||||||
@@ -309,7 +311,6 @@
|
|||||||
return `Unknown type: ${drop.type}`;
|
return `Unknown type: ${drop.type}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
purchaseGems () {
|
purchaseGems () {
|
||||||
this.$root.$emit('bv::show::modal', 'buy-gems');
|
this.$root.$emit('bv::show::modal', 'buy-gems');
|
||||||
},
|
},
|
||||||
|
|||||||
7
website/client/mixins/numberInvalid.js
Normal file
7
website/client/mixins/numberInvalid.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
numberInvalid () {
|
||||||
|
return this.selectedAmountToBuy < 1 || !Number.isInteger(this.selectedAmountToBuy);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ module.exports = {
|
|||||||
itemNotFound: 'Item "<%= key %>" not found.',
|
itemNotFound: 'Item "<%= key %>" not found.',
|
||||||
questNotFound: 'Quest "<%= key %>" not found.',
|
questNotFound: 'Quest "<%= key %>" not found.',
|
||||||
spellNotFound: 'Skill "<%= spellId %>" not found.',
|
spellNotFound: 'Skill "<%= spellId %>" not found.',
|
||||||
|
invalidQuantity: 'Quantity to purchase must be a positive whole number.',
|
||||||
invalidTypeEquip: '"type" must be one of "equipped", "pet", "mount", "costume"',
|
invalidTypeEquip: '"type" must be one of "equipped", "pet", "mount", "costume"',
|
||||||
missingPetFoodFeed: '"pet" and "food" are required parameters.',
|
missingPetFoodFeed: '"pet" and "food" are required parameters.',
|
||||||
missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.',
|
missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.',
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
"unlocked": "Items have been unlocked",
|
"unlocked": "Items have been unlocked",
|
||||||
"alreadyUnlocked": "Full set already unlocked.",
|
"alreadyUnlocked": "Full set already unlocked.",
|
||||||
"alreadyUnlockedPart": "Full set already partially unlocked.",
|
"alreadyUnlockedPart": "Full set already partially unlocked.",
|
||||||
"invalidQuantity": "Quantity to purchase must be a number.",
|
"invalidQuantity": "Quantity to purchase must be a positive whole number.",
|
||||||
|
|
||||||
"USD": "(USD)",
|
"USD": "(USD)",
|
||||||
"newStuff": "New Stuff by Bailey",
|
"newStuff": "New Stuff by Bailey",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class AbstractBuyOperation {
|
|||||||
let quantity = _get(req, 'quantity');
|
let quantity = _get(req, 'quantity');
|
||||||
|
|
||||||
this.quantity = quantity ? Number(quantity) : 1;
|
this.quantity = quantity ? Number(quantity) : 1;
|
||||||
if (isNaN(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity'));
|
if (this.quantity < 1 || !Number.isInteger(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ module.exports = function purchase (user, req = {}, analytics) {
|
|||||||
let key = get(req.params, 'key');
|
let key = get(req.params, 'key');
|
||||||
|
|
||||||
let quantity = req.quantity ? Number(req.quantity) : 1;
|
let quantity = req.quantity ? Number(req.quantity) : 1;
|
||||||
if (isNaN(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language));
|
if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language));
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
throw new BadRequest(i18n.t('typeRequired', req.language));
|
throw new BadRequest(i18n.t('typeRequired', req.language));
|
||||||
|
|||||||
BIN
website/raw_sprites/spritesmith_large/promo_butterflies.png
Normal file
BIN
website/raw_sprites/spritesmith_large/promo_butterflies.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 806 KiB |
@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
|
|||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
// @TODO export this const, cannot export it from here because only routes are exported from controllers
|
// @TODO export this const, cannot export it from here because only routes are exported from controllers
|
||||||
const LAST_ANNOUNCEMENT_TITLE = 'HABITICA BLOG: USE CASE SPOTLIGHT';
|
const LAST_ANNOUNCEMENT_TITLE = 'BEHIND THE SCENES: A BUTTERFLY GARDENING ADVENTURE WITH BEFFYMAROO!';
|
||||||
const worldDmg = { // @TODO
|
const worldDmg = { // @TODO
|
||||||
bailey: false,
|
bailey: false,
|
||||||
};
|
};
|
||||||
@@ -30,14 +30,13 @@ api.getNews = {
|
|||||||
<div class="mr-3 ${baileyClass}"></div>
|
<div class="mr-3 ${baileyClass}"></div>
|
||||||
<div class="media-body">
|
<div class="media-body">
|
||||||
<h1 class="align-self-center">${res.t('newStuff')}</h1>
|
<h1 class="align-self-center">${res.t('newStuff')}</h1>
|
||||||
<h2>4/18/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
<h2>4/23/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="scene_yesterdailies_repeatables center-block"></div>
|
<p>There's a new <a href='https://habitica.wordpress.com/2019/04/23/butterfly-gardening/' target='_blank'>Behind the Scenes post</a> on the Habitica Blog! Beffymaroo shares some information about starting your own butterfly garden and enjoying watching these fascinating--and beneficial--creatures in your home and yard.</p>
|
||||||
<p>This month's <a href='https://habitica.wordpress.com/2019/04/18/use-case-spotlight-reviewing-and-evaluating-your-tasks/' target='_blank'>Use Case Spotlight</a> is about Reviewing and Evaluating your Tasks! It features a number of great suggestions submitted by Habiticans in the <a href='/groups/guild/1d3a10bf-60aa-4806-a38b-82d1084a59e6'>Use Case Spotlights Guild</a>. We hope it helps any of you who might be considering a refresh for your Task Lists.</p>
|
<div class="small mb-3">by Beffymaroo</div>
|
||||||
<p>Plus, we're collecting user submissions for the next spotlight! How do you keep things fresh and interesting if you've been using Habitica for a long time? We’ll be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog 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!</p>
|
<div class="promo_butterflies center-block"></div>
|
||||||
<div class="small mb-3">by shanaqui</div>
|
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user