Compare commits

...

18 Commits

Author SHA1 Message Date
Hafiz
3d9cbaa42a Broken task confirmation modal 2025-12-02 13:18:57 -06:00
Hafiz
81b7af35ea lint 2025-11-21 13:22:04 -06:00
Hafiz
4468f06074 confirmation modals using existing components 2025-11-21 13:02:46 -06:00
Hafiz
2948824df7 Show currency amount on purchase confirmation modals 2025-11-18 14:02:53 -06:00
Hafiz
de1b509243 Fix top bar on color on confirmation prompt clipping 2025-11-18 13:40:11 -06:00
Hafiz
fd0dedf72e lint 2025-11-18 13:20:37 -06:00
Hafiz
8da456f4b7 Updated delete and confirmation modals 2025-11-18 13:15:43 -06:00
Hafiz
a22691d11f Fix confirmation modal not showing for items w/gems 2025-11-06 13:30:20 -06:00
Hafiz
1045a17354 lint 2025-11-06 13:10:49 -06:00
Hafiz
bf1ea90720 lint 2025-11-06 13:06:31 -06:00
Hafiz
ccc7e7d7a7 Confirmation prompts (replace browser confirmation prompts) 2025-11-06 12:59:57 -06:00
Hafiz
270ff2e034 Don't show orb of rebirth confirmation modal until page reloads 2025-11-04 12:49:41 -06:00
Hafiz
560dc5a896 Set and check rebirth confirmation modal from localstorage
Set and check rebirth confirmation modal from localstorage after window reload
2025-10-28 14:01:30 -05:00
Hafiz
c0d36db6af Merge remote-tracking branch 'origin/develop' into qa/shrimp 2025-10-28 13:30:41 -05:00
Hafiz
80de056bab Show orb of rebirth confirmation modal after use (window refresh) 2025-10-28 13:19:22 -05:00
Kalista Payne
b154fe2564 fix(faq): correct Markdown 2025-10-21 10:04:58 -05:00
Kalista Payne
a4a8dacc31 fix(link): direct to FAQ instead of wiki 2025-10-20 17:07:48 -05:00
Kalista Payne
c60822f44f fix(content): textual tweaks and updates 2025-10-20 17:02:55 -05:00
21 changed files with 2053 additions and 1359 deletions

View File

@@ -0,0 +1,10 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2649_1708)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27 36H21V30H27V36ZM48 6V42C48 45.3 45.3 48 42 48H6C2.7 48 0 45.3 0 42V6C0 2.7 2.7 0 6 0H42C45.3 0 48 2.7 48 6ZM42 6H6V42H42V6ZM27 12H21V27H27V12Z" fill="#DE3F3F"/>
</g>
<defs>
<clipPath id="clip0_2649_1708">
<rect width="48" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -1,8 +1,8 @@
<template>
<div
class="banner d-flex align-items-center justify-content-between py-3 px-4"
id="privacy-banner"
v-if="!hidden"
id="privacy-banner"
class="banner d-flex align-items-center justify-content-between py-3 px-4"
>
<p
class="mr-3 mb-0"

View File

@@ -328,6 +328,8 @@ export default {
alreadyReadNotification,
nextCron: null,
handledNotifications,
isInitialLoadComplete: false,
pendingRebirthNotification: null,
};
},
computed: {
@@ -453,6 +455,18 @@ export default {
return this.runYesterDailies();
},
async showPendingRebirthModal () {
if (this.pendingRebirthNotification) {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
await axios.post('/api/v4/notifications/read', {
notificationIds: [this.pendingRebirthNotification.id],
});
this.pendingRebirthNotification = null;
}
},
showDeathModal () {
this.playSound('Death');
this.$root.$emit('bv::show::modal', 'death');
@@ -661,6 +675,18 @@ export default {
this.showLevelUpNotifications(this.user.stats.lvl);
}
this.handleUserNotifications(this.user.notifications);
this.isInitialLoadComplete = true;
const hasRebirthConfirmationFlag = localStorage.getItem('show-rebirth-confirmation') === 'true';
if (hasRebirthConfirmationFlag) {
localStorage.removeItem('show-rebirth-confirmation');
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
} else {
this.showPendingRebirthModal();
}
},
async handleUserNotifications (after) {
if (this.$store.state.isRunningYesterdailies) return;
@@ -700,8 +726,15 @@ export default {
this.$root.$emit('habitica:won-challenge', notification);
break;
case 'REBIRTH_ACHIEVEMENT':
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
if (localStorage.getItem('show-rebirth-confirmation') === 'true') {
markAsRead = false;
} else if (!this.isInitialLoadComplete) {
this.pendingRebirthNotification = notification;
markAsRead = false;
} else {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
}
break;
case 'STREAK_ACHIEVEMENT':
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {

View File

@@ -1,231 +1,231 @@
<template>
<b-modal
id="buy-modal"
:hide-header="true"
@change="onChange($event)"
>
<span
v-if="withPin"
class="badge-dialog"
tabindex="0"
@click.prevent.stop="togglePinned()"
@keypress.enter.prevent.stop="togglePinned()"
id="buy-modal"
:hide-header="true"
@change="onChange($event)"
>
<pin-badge
:pinned="isPinned"
/>
</span>
<div>
<span
class="svg-icon close-icon icon-16 color"
aria-hidden="true"
v-if="withPin"
class="badge-dialog"
tabindex="0"
@click="hideDialog()"
@keypress.enter="hideDialog()"
v-html="icons.close"
></span>
</div>
<div
v-if="item != null"
class="content"
>
<div class="inner-content">
<slot
name="item"
:item="item"
>
<div v-if="showAvatar">
<avatar
:show-visual-buffs="false"
:member="user"
:avatar-only="true"
:hide-class-badge="true"
:with-background="true"
:override-avatar-gear="getAvatarOverrides(item)"
:sprites-margin="'0px auto 0px -24px'"
@click.prevent.stop="togglePinned()"
@keypress.enter.prevent.stop="togglePinned()"
>
<pin-badge
:pinned="isPinned"
/>
</span>
<div>
<span
class="svg-icon close-icon icon-16 color"
aria-hidden="true"
tabindex="0"
@click="hideDialog()"
@keypress.enter="hideDialog()"
v-html="icons.close"
></span>
</div>
<div
v-if="item != null"
class="content"
>
<div class="inner-content">
<slot
name="item"
:item="item"
>
<div v-if="showAvatar">
<avatar
:show-visual-buffs="false"
:member="user"
:avatar-only="true"
:hide-class-badge="true"
:with-background="true"
:override-avatar-gear="getAvatarOverrides(item)"
:sprites-margin="'0px auto 0px -24px'"
/>
</div>
<item
v-else-if="item.key === 'gem'"
class="flat bordered-item"
:item="item"
:item-content-class="item.class"
:show-popover="false"
/>
<item
v-else-if="item.key != 'gem'"
class="flat bordered-item"
:item="item"
:item-content-class="item.class"
:show-popover="false"
/>
</slot>
<div
v-if="!showAvatar && user.items[item.purchaseType]"
class="owned"
:class="totalOwned"
>
<!-- eslint-disable-next-line max-len -->
<span class="owned-text">{{ $t('owned') }}: <span class="user-amount">{{ totalOwned }}</span></span>
</div>
<item
v-else-if="item.key === 'gem'"
class="flat bordered-item"
<h4 class="title">
{{ itemText }}
</h4>
<div class="item-notes">
{{ itemNotes }}
</div>
<slot
name="additionalInfo"
:item="item"
:item-content-class="item.class"
:show-popover="false"
/>
<item
v-else-if="item.key != 'gem'"
class="flat bordered-item"
:item="item"
:item-content-class="item.class"
:show-popover="false"
/>
</slot>
<div
v-if="!showAvatar && user.items[item.purchaseType]"
class="owned"
:class="totalOwned"
>
<!-- eslint-disable-next-line max-len -->
<span class="owned-text">{{ $t('owned') }}: <span class="user-amount">{{ totalOwned }}</span></span>
</div>
<h4 class="title">
{{ itemText }}
</h4>
<div class="item-notes">
{{ itemNotes }}
</div>
<slot
name="additionalInfo"
:item="item"
>
<equipmentAttributesGrid
v-if="showAttributesGrid"
class="attributesGrid"
:item="item"
:user="user"
/>
</slot>
<div
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
class="purchase-amount"
>
<div class="item-cost justify-content-center my-3">
<span
class="cost d-flex mx-auto"
:class="getPriceClass()"
>
>
<equipmentAttributesGrid
v-if="showAttributesGrid"
class="attributesGrid"
:item="item"
:user="user"
/>
</slot>
<div
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
class="purchase-amount"
>
<div class="item-cost justify-content-center my-3">
<span
class="svg-icon icon-24 my-auto mr-1"
aria-hidden="true"
v-html="icons[getPriceClass()]"
class="cost d-flex mx-auto"
:class="getPriceClass()"
>
<span
class="svg-icon icon-24 my-auto mr-1"
aria-hidden="true"
v-html="icons[getPriceClass()]"
>
</span>
<span
class="my-auto"
:class="getPriceClass()"
>{{ item.value }}</span>
</span>
<span
class="my-auto"
:class="getPriceClass()"
>{{ item.value }}</span>
</span>
</div>
</div>
<div
v-if="showAmountToBuy(item)"
class="how-many-to-buy"
>
{{ $t('howManyToBuy') }}
</div>
<div
v-if="showAmountToBuy(item)"
>
<number-increment
class="number-increment"
@updateQuantity="selectedAmountToBuy = $event"
/>
<div
:class="{'notEnough': notEnoughCurrency}"
class="total"
v-if="showAmountToBuy(item)"
class="how-many-to-buy"
>
<span class="total-text">{{ $t('sendTotal') }}</span>
<span
class="svg-icon total icon-24"
aria-hidden="true"
v-html="icons[getPriceClass()]"
></span>
<span
class="total-text"
:class="getPriceClass()"
>{{ item.value * selectedAmountToBuy }}</span>
{{ $t('howManyToBuy') }}
</div>
<div
v-if="showAmountToBuy(item)"
>
<number-increment
class="number-increment"
@updateQuantity="selectedAmountToBuy = $event"
/>
<div
:class="{'notEnough': notEnoughCurrency}"
class="total"
>
<span class="total-text">{{ $t('sendTotal') }}</span>
<span
class="svg-icon total icon-24"
aria-hidden="true"
v-html="icons[getPriceClass()]"
></span>
<span
class="total-text"
:class="getPriceClass()"
>{{ item.value * selectedAmountToBuy }}</span>
</div>
</div>
</div>
<div
v-if="item.key === 'gem' && gemsLeft < 1"
class="no-more-gems"
>
{{ $t('notEnoughGemsToBuy') }}
</div>
<div
v-if="nonSubscriberHourglasses"
class="hourglass-nonsub mt-3"
>
{{ $t('mysticHourglassNeededNoSub') }}
</div>
<button
v-if="getPriceClass() === 'gems'
&& !enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
class="btn btn-primary mb-3"
@click="purchaseGems()"
>
{{ $t('purchaseGems') }}
</button>
<button
v-else-if="nonSubscriberHourglasses"
class="btn btn-primary"
@click="viewSubscriptions(item)"
>
{{ $t('viewSubscriptions') }}
</button>
<button
v-else-if="!(item.key === 'gem' && gemsLeft < 1)"
class="btn btn-primary"
:disabled="item.key === 'gem' && gemsLeft === 0 ||
attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid || item.locked ||
!preventHealthPotion ||
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
:class="{'notEnough': !preventHealthPotion ||
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
tabindex="0"
@click="buyItem()"
>
{{ $t('buyNow') }}
</button>
</div>
<div
v-if="item.key === 'gem' && gemsLeft < 1"
class="no-more-gems"
>
{{ $t('notEnoughGemsToBuy') }}
</div>
<div
v-if="nonSubscriberHourglasses"
class="hourglass-nonsub mt-3"
>
{{ $t('mysticHourglassNeededNoSub') }}
</div>
<button
v-if="getPriceClass() === 'gems'
&& !enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
class="btn btn-primary mb-3"
@click="purchaseGems()"
>
{{ $t('purchaseGems') }}
</button>
<button
v-else-if="nonSubscriberHourglasses"
class="btn btn-primary"
@click="viewSubscriptions(item)"
>
{{ $t('viewSubscriptions') }}
</button>
<button
v-else-if="!(item.key === 'gem' && gemsLeft < 1)"
class="btn btn-primary"
:disabled="item.key === 'gem' && gemsLeft === 0 ||
attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid || item.locked ||
!preventHealthPotion ||
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
:class="{'notEnough': !preventHealthPotion ||
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
tabindex="0"
@click="buyItem()"
>
{{ $t('buyNow') }}
</button>
</div>
</div>
<countdown-banner
v-if="item.end && item.owned == null"
:end-date="endDate"
class="limitedTime available"
/>
<div
v-if="item.key === 'rebirth_orb' && item.value > 0 && user.stats.lvl >= 100"
class="free-rebirth d-flex align-items-center"
>
<div class="m-auto">
<span
class="svg-icon inline icon-16 mr-2 pt-015"
v-html="icons.whiteClock"
></span>
<span v-html="$t('nextFreeRebirth', {days: nextFreeRebirth})"></span>
</div>
</div>
<div
v-if="item.key === 'gem'"
class="d-flex justify-content-center align-items-center"
>
<div
v-if="gemsLeft > 0"
class="gems-left d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
<div
v-if="gemsLeft === 0"
class="out-of-gems-banner d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
</div>
<div
slot="modal-footer"
>
<span class="user-balance ml-3 my-auto">{{ $t('yourBalance') }}</span>
<balanceInfo
class="mr-3"
:currency-needed="getPriceClass()"
:amount-needed="item.value"
<countdown-banner
v-if="item.end && item.owned == null"
:end-date="endDate"
class="limitedTime available"
/>
</div>
<div
v-if="item.key === 'rebirth_orb' && item.value > 0 && user.stats.lvl >= 100"
class="free-rebirth d-flex align-items-center"
>
<div class="m-auto">
<span
class="svg-icon inline icon-16 mr-2 pt-015"
v-html="icons.whiteClock"
></span>
<span v-html="$t('nextFreeRebirth', {days: nextFreeRebirth})"></span>
</div>
</div>
<div
v-if="item.key === 'gem'"
class="d-flex justify-content-center align-items-center"
>
<div
v-if="gemsLeft > 0"
class="gems-left d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
<div
v-if="gemsLeft === 0"
class="out-of-gems-banner d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
</div>
<div
slot="modal-footer"
>
<span class="user-balance ml-3 my-auto">{{ $t('yourBalance') }}</span>
<balanceInfo
class="mr-3"
:currency-needed="getPriceClass()"
:amount-needed="item.value"
/>
</div>
</b-modal>
</template>
@@ -851,10 +851,17 @@ export default {
- ownedMounts
- ownedItems;
if (
petsRemaining < 0
&& !window.confirm(this.$t('purchasePetItemConfirm', { itemText: this.item.text })) // eslint-disable-line no-alert
) return;
if (petsRemaining < 0) {
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:purchase-confirm', {
message: this.$t('purchasePetItemConfirm', { itemText: this.item.text }),
currency: this.item.currency,
cost: this.item.value * this.selectedAmountToBuy,
resolve,
});
});
if (!confirmed) return;
}
}
if (this.item.purchaseType === 'customization') {
@@ -866,15 +873,23 @@ export default {
this.purchased(this.item.text);
} else {
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
if (
shouldConfirmPurchase
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
) {
return;
if (shouldConfirmPurchase) {
const confirmed = await this.confirmPurchase(
this.item.currency,
this.item.value * this.selectedAmountToBuy,
);
if (!confirmed) {
return;
}
}
if (this.genericPurchase) {
if (this.item.key === 'rebirth_orb') {
localStorage.setItem('show-rebirth-confirmation', 'true');
}
await this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
await this.purchased(this.item.text);
if (this.item.key !== 'rebirth_orb') {
await this.purchased(this.item.text);
}
}
}

View File

@@ -0,0 +1,207 @@
<template>
<b-modal
id="purchase-confirm-modal"
:hide-footer="true"
:hide-header="true"
modal-class="purchase-confirm-modal"
centered
>
<div class="modal-content-wrapper">
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="currency-chip"
:class="currency"
>
<span
class="svg-icon icon-24"
v-html="icons[currency]"
></span>
<span class="cost-value">{{ cost }}</span>
</div>
<h2 class="modal-title">
{{ $t('confirmPurchase') }}
</h2>
<p class="modal-subtitle">
{{ confirmationMessage }}
</p>
<div class="button-wrapper">
<button
class="btn btn-primary"
@click="confirm()"
>
{{ $t('confirm') }}
</button>
<button
class="btn-cancel"
@click="cancel()"
>
{{ $t('cancel') }}
</button>
</div>
</div>
</div>
</b-modal>
</template>
<script>
import svgGem from '@/assets/svg/gem.svg?raw';
import svgHourglass from '@/assets/svg/hourglass.svg?raw';
export default {
data () {
return {
confirmationMessage: '',
currency: 'gems',
cost: 0,
resolveCallback: null,
icons: Object.freeze({
gems: svgGem,
hourglasses: svgHourglass,
}),
};
},
mounted () {
this.$root.$on('habitica:purchase-confirm', config => {
this.confirmationMessage = config.message;
this.currency = config.currency || 'gems';
this.cost = config.cost || 0;
this.resolveCallback = config.resolve;
this.$root.$emit('bv::show::modal', 'purchase-confirm-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:purchase-confirm');
},
methods: {
confirm () {
if (this.resolveCallback) {
this.resolveCallback(true);
}
this.close();
},
cancel () {
if (this.resolveCallback) {
this.resolveCallback(false);
}
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'purchase-confirm-modal');
},
},
};
</script>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .purchase-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $purple-300;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.currency-chip {
display: flex;
align-items: center;
gap: 8px;
margin-top: 40px;
padding: 8px 20px;
border-radius: 20px;
font-size: 1.25rem;
font-weight: bold;
line-height: 1.4;
&.gems {
color: $gems-color;
background-color: rgba($green-10, 0.15);
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba($blue-10, 0.15);
}
.icon-24 {
width: 24px;
height: 24px;
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $purple-300;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>

View File

@@ -1,119 +1,119 @@
<template>
<b-modal
id="buy-quest-modal"
:hide-header="true"
@change="onChange($event)"
>
<span
v-if="withPin"
class="badge-dialog"
@click.prevent.stop="togglePinned()"
id="buy-quest-modal"
:hide-header="true"
@change="onChange($event)"
>
<pin-badge
:pinned="isPinned"
/>
</span>
<div class="dialog-close">
<close-icon @click="hideDialog()" />
</div>
<h2 class="text-center textCondensed">
{{ $t('questDetails') }}
</h2>
<div
v-if="item != null"
class="content"
>
<div class="inner-content">
<questDialogContent
:item="item"
:abbreviated="true"
<span
v-if="withPin"
class="badge-dialog"
@click.prevent.stop="togglePinned()"
>
<pin-badge
:pinned="isPinned"
/>
<div
v-if="item.addlNotes"
class="mx-4 mb-3"
>
{{ item.addlNotes }}
</div>
<quest-rewards :quest="item" />
<div
v-if="!item.locked"
class="purchase-amount"
>
<div class="item-cost">
<span
class="cost"
:class="priceType"
>
</span>
<div class="dialog-close">
<close-icon @click="hideDialog()" />
</div>
<h2 class="text-center textCondensed">
{{ $t('questDetails') }}
</h2>
<div
v-if="item != null"
class="content"
>
<div class="inner-content">
<questDialogContent
:item="item"
:abbreviated="true"
/>
<div
v-if="item.addlNotes"
class="mx-4 mb-3"
>
{{ item.addlNotes }}
</div>
<quest-rewards :quest="item" />
<div
v-if="!item.locked"
class="purchase-amount"
>
<div class="item-cost">
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[priceType]"
class="cost"
:class="priceType"
>
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[priceType]"
>
</span>
<span
:class="priceType"
>{{ item.value }}</span>
</span>
</div>
<div class="how-many-to-buy">
<strong>{{ $t('howManyToBuy') }}</strong>
</div>
<div>
<number-increment
@updateQuantity="selectedAmountToBuy = $event"
/>
</div>
<div class="total-row">
<span class="total-text">
{{ $t('sendTotal') }}
</span>
<span
class="svg-icon inline icon-20"
aria-hidden="true"
v-html="currencyIcon"
></span>
<span
class="total"
:class="priceType"
>{{ item.value }}</span>
</span>
</div>
<div class="how-many-to-buy">
<strong>{{ $t('howManyToBuy') }}</strong>
</div>
<div>
<number-increment
@updateQuantity="selectedAmountToBuy = $event"
/>
</div>
<div class="total-row">
<span class="total-text">
{{ $t('sendTotal') }}
</span>
<span
class="svg-icon inline icon-20"
aria-hidden="true"
v-html="currencyIcon"
></span>
<span
class="total"
:class="priceType"
>{{ item.value * selectedAmountToBuy }}</span>
>{{ item.value * selectedAmountToBuy }}</span>
</div>
</div>
<button
v-if="priceType === 'gems'
&& !enoughCurrency(priceType, item.value * selectedAmountToBuy)
&& !item.locked"
class="btn btn-primary mb-3"
@click="purchaseGems()"
>
{{ $t('purchaseGems') }}
</button>
<button
v-else
class="btn btn-primary mb-4"
:class="{'notEnough': !enoughCurrency(priceType, item.value * selectedAmountToBuy)}"
:disabled="numberInvalid"
@click="buyItem()"
>
{{ $t('buyNow') }}
</button>
</div>
<button
v-if="priceType === 'gems'
&& !enoughCurrency(priceType, item.value * selectedAmountToBuy)
&& !item.locked"
class="btn btn-primary mb-3"
@click="purchaseGems()"
>
{{ $t('purchaseGems') }}
</button>
<button
v-else
class="btn btn-primary mb-4"
:class="{'notEnough': !enoughCurrency(priceType, item.value * selectedAmountToBuy)}"
:disabled="numberInvalid"
@click="buyItem()"
>
{{ $t('buyNow') }}
</button>
</div>
</div>
<countdown-banner
v-if="item.end"
:end-date="endDate"
/>
<div
slot="modal-footer"
class="clearfix"
>
<span class="balance float-left">{{ $t('yourBalance') }}</span>
<balanceInfo
class="float-right"
:with-hourglass="priceType === 'hourglasses'"
:currency-needed="priceType"
:amount-needed="item.value"
<countdown-banner
v-if="item.end"
:end-date="endDate"
/>
</div>
<div
slot="modal-footer"
class="clearfix"
>
<span class="balance float-left">{{ $t('yourBalance') }}</span>
<balanceInfo
class="float-right"
:with-hourglass="priceType === 'hourglasses'"
:currency-needed="priceType"
:amount-needed="item.value"
/>
</div>
</b-modal>
</template>
@@ -510,8 +510,12 @@ export default {
this.selectedAmountToBuy = 1;
this.$emit('change', $event);
},
buyItem () {
if (!this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)) {
async buyItem () {
const confirmed = await this.confirmPurchase(
this.item.currency,
this.item.value * this.selectedAmountToBuy,
);
if (!confirmed) {
return;
}
this.makeGenericPurchase(this.item, 'buyQuestModal', this.selectedAmountToBuy);

View File

@@ -64,9 +64,11 @@
<li>sexual orientation; and</li>
<li>information collected from a known child.</li>
</ul>
<p><strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong></p>
<p>
<strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong>
</p>
<h3 id="section_1_1">
1.1 Information You Provide Directly
</h3>
@@ -617,7 +619,7 @@
7. General Audience Services
</h2>
<p>
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>, and we will delete that information from our databases.
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>, and we will delete that information from our databases.
</p>
<h2 id="section_8">
@@ -708,7 +710,7 @@
<p><strong><u>Nevada Residents</u></strong></p>
<p>
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>.
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>.
</p>
<p><strong><u>Notice to United Kingdom/European/Switzerland Residents.</u></strong></p>
<p>

View File

@@ -15,8 +15,8 @@
<router-view />
</div>
<div
id="bottom-background"
v-if="loginFlow"
id="bottom-background"
class="bg-purple-300"
>
<div class="seamless_mountains_demo_repeat"></div>
@@ -31,7 +31,10 @@
id="bottom-wrap"
class="purple-4"
>
<div id="bottom-background" v-if="!loginFlow">
<div
v-if="!loginFlow"
id="bottom-background"
>
<div class="seamless_mountains_demo_repeat"></div>
<div class="midground_foreground_extended2"></div>
</div>

View File

@@ -158,7 +158,7 @@
BY PURCHASING PREMIUM YOU EXPRESSLY UNDERSTAND AND AGREE TO OUR REFUND POLICY:
</p>
<p>
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href='mailto:admin@habitica.com'>ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href="mailto:admin@habitica.com">ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
</p>
<p>
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a

View File

@@ -1,65 +1,45 @@
<template>
<b-modal
id="broken-task-modal"
title="Broken Challenge"
size="sm"
:hide-footer="true"
:hide-header="true"
modal-class="broken-task-confirm-modal"
centered
>
<div
v-if="brokenChallengeTask && brokenChallengeTask.challenge"
class="modal-body"
class="modal-content-wrapper"
>
<div
v-if="brokenChallengeTask.challenge.broken === 'TASK_DELETED'
|| brokenChallengeTask.challenge.broken === 'CHALLENGE_TASK_NOT_FOUND'"
>
<h2>{{ $t('brokenTask') }}</h2>
<div>
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="icon-wrapper"
v-html="icons.warningIcon"
></div>
<h2 class="modal-title">
{{ modalTitle }}
</h2>
<p class="modal-subtitle">
{{ modalSubtitle }}
</p>
<div class="button-wrapper">
<button
class="btn btn-primary"
@click="unlink('keep')"
@click="keepAction()"
>
{{ $t('keepIt') }}
{{ keepButtonText }}
</button>
<button
class="btn btn-danger"
@click="removeTask(obj)"
@click="removeAction()"
>
{{ $t('removeIt') }}
</button>
</div>
</div>
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_DELETED'">
<h2>{{ $t('brokenChallenge') }}</h2>
<div>
<button
class="btn btn-primary"
@click="unlink('keep-all')"
>
{{ $t('keepTasks') }}
{{ removeButtonText }}
</button>
<button
class="btn btn-danger"
@click="unlink('remove-all')"
class="btn-cancel"
@click="close()"
>
{{ $t('removeTasks') }}
</button>
</div>
</div>
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_CLOSED'">
<h2 v-html="$t('challengeCompleted', {user: brokenChallengeTask.challenge.winner})"></h2>
<div>
<button
class="btn btn-primary"
@click="unlink('keep-all')"
>
{{ $t('keepTasks') }}
</button>
<button
class="btn btn-danger"
@click="unlink('remove-all')"
>
{{ $t('removeTasks') }}
{{ $t('cancel') }}
</button>
</div>
</div>
@@ -67,23 +47,171 @@
</b-modal>
</template>
<style scoped>
.modal-body {
padding-bottom: 2em;
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .broken-task-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $maroon-100;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.icon-wrapper {
margin-top: 40px;
width: 48px;
height: 48px;
::v-deep svg {
width: 48px;
height: 48px;
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $maroon-100;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>
<script>
import { mapActions } from '@/libs/store';
import notifications from '@/mixins/notifications';
import warningIcon from '@/assets/svg/warning_icon.svg?raw';
export default {
mixins: [notifications],
data () {
return {
brokenChallengeTask: {},
icons: Object.freeze({
warningIcon,
}),
};
},
computed: {
brokenType () {
return this.brokenChallengeTask.challenge?.broken;
},
isSingleTask () {
return this.brokenType === 'TASK_DELETED'
|| this.brokenType === 'CHALLENGE_TASK_NOT_FOUND';
},
brokenChallengeTaskCount () {
if (!this.brokenChallengeTask.challenge?.id) return 0;
const challengeId = this.brokenChallengeTask.challenge.id;
const tasksData = this.$store.state.tasks.data;
let count = 0;
['habits', 'dailys', 'todos', 'rewards'].forEach(type => {
if (tasksData[type]) {
count += tasksData[type].filter(
t => t.challenge && t.challenge.id === challengeId,
).length;
}
});
return count;
},
modalTitle () {
if (this.isSingleTask) {
return this.$t('brokenTask');
}
if (this.brokenType === 'CHALLENGE_CLOSED') {
return this.$t('challengeCompleted');
}
return this.$t('brokenChallenge');
},
modalSubtitle () {
if (this.isSingleTask) {
return this.$t('brokenTaskDescription');
}
if (this.brokenType === 'CHALLENGE_CLOSED') {
return this.$t('challengeCompletedDescription', { user: this.brokenChallengeTask.challenge?.winner });
}
return this.$t('brokenChallengeDescription');
},
keepButtonText () {
if (this.isSingleTask) {
return this.$t('keepIt');
}
return this.$t('keepTasks');
},
removeButtonText () {
if (this.isSingleTask) {
return this.$t('removeIt');
}
return this.$t('removeTasks');
},
},
mounted () {
this.$root.$on('handle-broken-task', task => {
this.brokenChallengeTask = { ...task };
@@ -99,8 +227,36 @@ export default {
unlinkOneTask: 'tasks:unlinkOneTask',
unlinkAllTasks: 'tasks:unlinkAllTasks',
}),
keepAction () {
if (this.isSingleTask) {
this.unlink('keep');
} else {
this.unlink('keep-all');
}
},
async removeAction () {
if (this.isSingleTask) {
await this.removeTask();
} else {
await this.unlink('remove-all');
}
},
async unlink (keepOption) {
if (keepOption.indexOf('-all') !== -1) {
if (keepOption === 'remove-all') {
const count = this.brokenChallengeTaskCount;
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
title: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
description: this.$t('brokenChallengeTaskCount', { count }),
message: this.$t('confirmDeleteTasks'),
buttonText: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
resolve,
});
});
if (!confirmed) return;
}
await this.unlinkAllTasks({
challengeId: this.brokenChallengeTask.challenge.id,
keep: keepOption,
@@ -122,8 +278,14 @@ export default {
});
this.close();
},
removeTask () {
if (!window.confirm('Are you sure you want to delete this task?')) return; // eslint-disable-line no-alert
async removeTask () {
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDelete'),
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.brokenChallengeTask);
this.close();
},

View File

@@ -87,7 +87,7 @@
ref="tasksList"
class="sortable-tasks"
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
scrollSensitivity="64"
scroll-sensitivity="64"
:delay-on-touch-only="true"
:delay="100"
@update="taskSorted"

View File

@@ -0,0 +1,219 @@
<template>
<b-modal
id="delete-task-confirm-modal"
:hide-footer="true"
:hide-header="true"
modal-class="delete-confirm-modal"
centered
>
<div class="modal-content-wrapper">
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="icon-wrapper"
v-html="icons.warningIcon"
></div>
<h2 class="modal-title">
{{ displayTitle }}
</h2>
<p
v-if="description"
class="modal-description"
>
{{ description }}
</p>
<p class="modal-subtitle">
{{ confirmationMessage }}
</p>
<div class="button-wrapper">
<button
class="btn btn-danger"
@click="confirm()"
>
{{ buttonText }}
</button>
<button
class="btn-cancel"
@click="cancel()"
>
{{ $t('cancel') }}
</button>
</div>
</div>
</div>
</b-modal>
</template>
<script>
import warningIcon from '@/assets/svg/warning_icon.svg?raw';
export default {
data () {
return {
confirmationMessage: '',
taskType: '',
description: '',
customTitle: '',
customButtonText: '',
resolveCallback: null,
icons: Object.freeze({
warningIcon,
}),
};
},
computed: {
displayTitle () {
if (this.customTitle) return this.customTitle;
return this.$t('deleteType', { type: this.taskType });
},
buttonText () {
if (this.customButtonText) return this.customButtonText;
return this.displayTitle;
},
},
mounted () {
this.$root.$on('habitica:delete-task-confirm', config => {
this.confirmationMessage = config.message;
this.taskType = config.taskType || '';
this.description = config.description || '';
this.customTitle = config.title || '';
this.customButtonText = config.buttonText || '';
this.resolveCallback = config.resolve;
this.$root.$emit('bv::show::modal', 'delete-task-confirm-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:delete-task-confirm');
},
methods: {
confirm () {
if (this.resolveCallback) {
this.resolveCallback(true);
}
this.close();
},
cancel () {
if (this.resolveCallback) {
this.resolveCallback(false);
}
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'delete-task-confirm-modal');
},
},
};
</script>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .delete-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $maroon-100;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.icon-wrapper {
margin-top: 40px;
width: 48px;
height: 48px;
::v-deep svg {
width: 48px;
height: 48px;
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $maroon-100;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-description {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.modal-description + .modal-subtitle {
margin-top: 16px;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>

View File

@@ -3,421 +3,421 @@
class="task-wrapper"
draggable
>
<div
class="task transition"
:class="[{
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess,
'task-not-scoreable': showTaskLockIcon,
'link-exempt': !isChallengeTask && !isGroupTask,
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
>
<div
class="d-flex"
:class="{'task-not-scoreable': showTaskLockIcon }"
class="task transition"
:class="[{
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess,
'task-not-scoreable': showTaskLockIcon,
'link-exempt': !isChallengeTask && !isGroupTask,
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
>
<!-- Habits left side control-->
<div
v-if="task.type === 'habit'"
class="left-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass
}, controlClass.up.bg]"
class="d-flex"
:class="{'task-not-scoreable': showTaskLockIcon }"
>
<!-- Habits left side control-->
<div
class="task-control habit-control"
v-if="task.type === 'habit'"
class="left-control d-flex justify-content-center pt-3"
:class="[{
'habit-control-positive-enabled': task.up && !showTaskLockIcon,
'habit-control-positive-disabled': !task.up && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.up.inner]"
tabindex="0"
role="button"
:aria-label="$t('scoreUp')"
:aria-disabled="showTaskLockIcon || (!task.up && !showTaskLockIcon)"
@click="score('up')"
@keypress.enter="score('up')"
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass
}, controlClass.up.bg]"
>
<div
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.up ? controlClass.up.icon : 'positive'"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon positive"
v-html="icons.positive"
></div>
</div>
</div>
<!-- Dailies and todos left side control-->
<div
v-if="task.type === 'daily' || task.type === 'todo'"
class="left-control d-flex justify-content-center"
:class="[{
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.bg]"
>
<div
class="task-control daily-todo-control"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.inner,
]"
tabindex="0"
role="checkbox"
@click="score(showCheckIcon ? 'down' : 'up' )"
@keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
>
<div
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="controlClass.icon"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon check"
:class="{
'display-check-icon': showCheckIcon,
[controlClass.checkbox]: true,
}"
v-html="icons.check"
></div>
</div>
</div>
<!-- Task title, description and icons-->
<div
class="task-content"
:class="contentClass"
>
<div
class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)"
@keypress.enter="edit($event, task)"
>
<div class="d-flex justify-content-between">
<h3
v-markdown="task.text"
class="task-title markdown"
:class="{ 'has-notes': task.notes }"
></h3>
<menu-dropdown
v-if="!isRunningYesterdailies && showOptions"
ref="taskDropdown"
v-b-tooltip.hover.top="$t('options')"
tabindex="0"
class="task-dropdown mr-1"
:right="task.type === 'reward'"
>
<div slot="dropdown-toggle">
<div
class="svg-icon dropdown-icon"
v-html="icons.menu"
></div>
</div>
<div slot="dropdown-content">
<div
v-if="showEdit"
ref="editTaskItem"
class="dropdown-item edit-task-item"
tabindex="0"
@keypress.enter="edit($event, task)"
>
<span class="dropdown-icon-item">
<span
class="svg-icon inline edit-icon"
v-html="icons.edit"
></span>
<span class="text">{{ $t('edit') }}</span>
</span>
</div>
<div
class="dropdown-item"
tabindex="0"
@click="moveToTop"
@keypress.enter="moveToTop"
>
<span class="dropdown-icon-item">
<span
class="svg-icon inline push-to-top"
v-html="icons.top"
></span>
<span class="text">{{ $t('taskToTop') }}</span>
</span>
</div>
<div
class="dropdown-item"
tabindex="0"
@click="moveToBottom"
@keypress.enter="moveToBottom"
>
<span class="dropdown-icon-item">
<span
class="svg-icon inline push-to-bottom"
v-html="icons.bottom"
></span>
<span class="text">{{ $t('taskToBottom') }}</span>
</span>
</div>
<div
v-if="showDelete"
class="dropdown-item"
tabindex="0"
@click="destroy"
@keypress.enter="destroy"
>
<span class="dropdown-icon-item delete-task-item">
<span
class="svg-icon inline delete"
v-html="icons.delete"
></span>
<span class="text">{{ $t('delete') }}</span>
</span>
</div>
</div>
</menu-dropdown>
</div>
<div
v-markdown="task.notes"
class="task-notes small-text"
:class="{'has-checklist': task.notes && hasChecklist}"
></div>
</div>
<div
v-if="canViewchecklist"
class="checklist"
:class="{isOpen: !task.collapseChecklist}"
>
<div class="d-inline-flex">
<div
v-b-tooltip.hover.right="$t(`${task.collapseChecklist
? 'expand': 'collapse'}Checklist`)"
class="collapse-checklist mb-2 d-flex align-items-center expand-toggle"
:class="{open: !task.collapseChecklist}"
tabindex="0"
@click="collapseChecklist(task)"
@keypress.enter="collapseChecklist(task)"
>
<div
v-once
class="svg-icon"
v-html="icons.checklist"
></div>
<span>{{ checklistProgress }}</span>
</div>
</div>
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="item in task.checklist"
v-if="!task.collapseChecklist"
:key="item.id"
class="custom-control custom-checkbox checklist-item"
:class="{'checklist-item-done': item.completed, 'cursor-auto': showTaskLockIcon}"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<input
:id="`checklist-${item.id}-${random}`"
class="custom-control-input"
tabindex="0"
type="checkbox"
:checked="item.completed"
:disabled="castingSpell || showTaskLockIcon"
@change="toggleChecklistItem(item)"
@keypress.enter="toggleChecklistItem(item)"
>
<label
v-markdown="item.text"
class="custom-control-label"
:class="{ 'cursor-auto': showTaskLockIcon }"
:for="`checklist-${item.id}-${random}`"
></label>
</div>
</div>
<div class="icons small-text d-flex align-items-center">
<div
v-if="task.type === 'todo' && task.date"
class="d-flex align-items-center"
:class="{'due-overdue': checkIfOverdue() }"
class="task-control habit-control"
:class="[{
'habit-control-positive-enabled': task.up && !showTaskLockIcon,
'habit-control-positive-disabled': !task.up && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.up.inner]"
tabindex="0"
role="button"
:aria-label="$t('scoreUp')"
:aria-disabled="showTaskLockIcon || (!task.up && !showTaskLockIcon)"
@click="score('up')"
@keypress.enter="score('up')"
>
<div
v-b-tooltip.hover.bottom="$t('dueDate')"
class="svg-icon calendar my-auto"
v-html="icons.calendar"
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.up ? controlClass.up.icon : 'positive'"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon positive"
v-html="icons.positive"
></div>
<span>{{ formatDueDate() }}</span>
</div>
<div class="icons-right d-flex justify-content-end">
</div>
<!-- Dailies and todos left side control-->
<div
v-if="task.type === 'daily' || task.type === 'todo'"
class="left-control d-flex justify-content-center"
:class="[{
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.bg]"
>
<div
class="task-control daily-todo-control"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.inner,
]"
tabindex="0"
role="checkbox"
@click="score(showCheckIcon ? 'down' : 'up' )"
@keypress.enter="score(showCheckIcon ? 'down' : 'up' )"
>
<div
v-if="showStreak"
class="d-flex align-items-center"
>
<div
v-b-tooltip.hover.bottom="task.type === 'daily'
? $t('streakCounter') : $t('counter')"
class="svg-icon streak"
v-html="icons.streak"
></div>
<span v-if="task.type === 'daily'">{{ task.streak }}</span>
<span v-if="task.type === 'habit'">
<span
v-if="task.up && task.counterUp != 0 && task.down"
class="m-0"
>+{{ task.counterUp }}</span>
<span
v-else-if=" task.counterUp !=0 && task.counterDown ==0"
class="m-0"
>{{ task.counterUp }}</span>
<span
v-else-if="task.up"
class="m-0"
>0</span>
<span
v-if="task.up && task.down"
class="m-0"
>&nbsp;|&nbsp;</span>
<span
v-if="task.down && task.counterDown != 0 && task.up"
class="m-0"
>-{{ task.counterDown }}</span>
<span
v-else-if="task.counterDown !=0 && task.counterUp ==0"
class="m-0"
>{{ task.counterDown }}</span>
<span
v-else-if="task.down"
class="m-0"
>0</span>
</span>
</div>
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="controlClass.icon"
v-html="icons.lock"
></div>
<div
v-if="task.challenge && task.challenge.id"
class="d-flex align-items-center"
>
<div
v-if="!task.challenge.broken"
v-b-tooltip.hover.bottom="shortName"
class="svg-icon challenge"
v-html="icons.challenge"
></div>
<div
v-if="task.challenge.broken"
v-b-tooltip.hover.bottom="$t('brokenChaLink')"
class="svg-icon challenge broken"
@click="handleBrokenTask(task)"
v-html="icons.brokenChallengeIcon"
></div>
</div>
<div
v-if="hasTags && !task.group.id"
:id="`tags-icon-${task._id}`"
class="d-flex align-items-center"
>
<div
class="svg-icon tags"
v-html="icons.tags"
></div>
</div>
<b-popover
v-if="hasTags && !task.group.id"
:target="`tags-icon-${task._id}`"
triggers="hover"
placement="bottom"
>
<div class="tags-popover">
<div class="d-flex align-items-center tags-container">
v-else
class="svg-icon check"
:class="{
'display-check-icon': showCheckIcon,
[controlClass.checkbox]: true,
}"
v-html="icons.check"
></div>
</div>
</div>
<!-- Task title, description and icons-->
<div
class="task-content"
:class="contentClass"
>
<div
class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)"
@keypress.enter="edit($event, task)"
>
<div class="d-flex justify-content-between">
<h3
v-markdown="task.text"
class="task-title markdown"
:class="{ 'has-notes': task.notes }"
></h3>
<menu-dropdown
v-if="!isRunningYesterdailies && showOptions"
ref="taskDropdown"
v-b-tooltip.hover.top="$t('options')"
tabindex="0"
class="task-dropdown mr-1"
:right="task.type === 'reward'"
>
<div slot="dropdown-toggle">
<div
v-once
class="tags-popover-title"
>
{{ `${$t('tags')}:` }}
</div>
<div
v-for="tag in getTagsFor(task)"
:key="tag"
v-markdown="tag"
class="tag-label"
class="svg-icon dropdown-icon"
v-html="icons.menu"
></div>
</div>
<div slot="dropdown-content">
<div
v-if="showEdit"
ref="editTaskItem"
class="dropdown-item edit-task-item"
tabindex="0"
@keypress.enter="edit($event, task)"
>
<span class="dropdown-icon-item">
<span
class="svg-icon inline edit-icon"
v-html="icons.edit"
></span>
<span class="text">{{ $t('edit') }}</span>
</span>
</div>
<div
class="dropdown-item"
tabindex="0"
@click="moveToTop"
@keypress.enter="moveToTop"
>
<span class="dropdown-icon-item">
<span
class="svg-icon inline push-to-top"
v-html="icons.top"
></span>
<span class="text">{{ $t('taskToTop') }}</span>
</span>
</div>
<div
class="dropdown-item"
tabindex="0"
@click="moveToBottom"
@keypress.enter="moveToBottom"
>
<span class="dropdown-icon-item">
<span
class="svg-icon inline push-to-bottom"
v-html="icons.bottom"
></span>
<span class="text">{{ $t('taskToBottom') }}</span>
</span>
</div>
<div
v-if="showDelete"
class="dropdown-item"
tabindex="0"
@click="destroy"
@keypress.enter="destroy"
>
<span class="dropdown-icon-item delete-task-item">
<span
class="svg-icon inline delete"
v-html="icons.delete"
></span>
<span class="text">{{ $t('delete') }}</span>
</span>
</div>
</div>
</menu-dropdown>
</div>
<div
v-markdown="task.notes"
class="task-notes small-text"
:class="{'has-checklist': task.notes && hasChecklist}"
></div>
</div>
<div
v-if="canViewchecklist"
class="checklist"
:class="{isOpen: !task.collapseChecklist}"
>
<div class="d-inline-flex">
<div
v-b-tooltip.hover.right="$t(`${task.collapseChecklist
? 'expand': 'collapse'}Checklist`)"
class="collapse-checklist mb-2 d-flex align-items-center expand-toggle"
:class="{open: !task.collapseChecklist}"
tabindex="0"
@click="collapseChecklist(task)"
@keypress.enter="collapseChecklist(task)"
>
<div
v-once
class="svg-icon"
v-html="icons.checklist"
></div>
<span>{{ checklistProgress }}</span>
</div>
</b-popover>
</div>
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
v-for="item in task.checklist"
v-if="!task.collapseChecklist"
:key="item.id"
class="custom-control custom-checkbox checklist-item"
:class="{'checklist-item-done': item.completed, 'cursor-auto': showTaskLockIcon}"
>
<!-- eslint-enable vue/no-use-v-if-with-v-for -->
<input
:id="`checklist-${item.id}-${random}`"
class="custom-control-input"
tabindex="0"
type="checkbox"
:checked="item.completed"
:disabled="castingSpell || showTaskLockIcon"
@change="toggleChecklistItem(item)"
@keypress.enter="toggleChecklistItem(item)"
>
<label
v-markdown="item.text"
class="custom-control-label"
:class="{ 'cursor-auto': showTaskLockIcon }"
:for="`checklist-${item.id}-${random}`"
></label>
</div>
</div>
<div class="icons small-text d-flex align-items-center">
<div
v-if="task.type === 'todo' && task.date"
class="d-flex align-items-center"
:class="{'due-overdue': checkIfOverdue() }"
>
<div
v-b-tooltip.hover.bottom="$t('dueDate')"
class="svg-icon calendar my-auto"
v-html="icons.calendar"
></div>
<span>{{ formatDueDate() }}</span>
</div>
<div class="icons-right d-flex justify-content-end">
<div
v-if="showStreak"
class="d-flex align-items-center"
>
<div
v-b-tooltip.hover.bottom="task.type === 'daily'
? $t('streakCounter') : $t('counter')"
class="svg-icon streak"
v-html="icons.streak"
></div>
<span v-if="task.type === 'daily'">{{ task.streak }}</span>
<span v-if="task.type === 'habit'">
<span
v-if="task.up && task.counterUp != 0 && task.down"
class="m-0"
>+{{ task.counterUp }}</span>
<span
v-else-if=" task.counterUp !=0 && task.counterDown ==0"
class="m-0"
>{{ task.counterUp }}</span>
<span
v-else-if="task.up"
class="m-0"
>0</span>
<span
v-if="task.up && task.down"
class="m-0"
>&nbsp;|&nbsp;</span>
<span
v-if="task.down && task.counterDown != 0 && task.up"
class="m-0"
>-{{ task.counterDown }}</span>
<span
v-else-if="task.counterDown !=0 && task.counterUp ==0"
class="m-0"
>{{ task.counterDown }}</span>
<span
v-else-if="task.down"
class="m-0"
>0</span>
</span>
</div>
<div
v-if="task.challenge && task.challenge.id"
class="d-flex align-items-center"
>
<div
v-if="!task.challenge.broken"
v-b-tooltip.hover.bottom="shortName"
class="svg-icon challenge"
v-html="icons.challenge"
></div>
<div
v-if="task.challenge.broken"
v-b-tooltip.hover.bottom="$t('brokenChaLink')"
class="svg-icon challenge broken"
@click="handleBrokenTask(task)"
v-html="icons.brokenChallengeIcon"
></div>
</div>
<div
v-if="hasTags && !task.group.id"
:id="`tags-icon-${task._id}`"
class="d-flex align-items-center"
>
<div
class="svg-icon tags"
v-html="icons.tags"
></div>
</div>
<b-popover
v-if="hasTags && !task.group.id"
:target="`tags-icon-${task._id}`"
triggers="hover"
placement="bottom"
>
<div class="tags-popover">
<div class="d-flex align-items-center tags-container">
<div
v-once
class="tags-popover-title"
>
{{ `${$t('tags')}:` }}
</div>
<div
v-for="tag in getTagsFor(task)"
:key="tag"
v-markdown="tag"
class="tag-label"
></div>
</div>
</div>
</b-popover>
</div>
</div>
</div>
</div>
<!-- Habits right side control-->
<div
v-if="task.type === 'habit'"
class="right-control d-flex justify-content-center pt-3"
:class="[{
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.down.bg]"
>
<!-- Habits right side control-->
<div
class="task-control habit-control"
v-if="task.type === 'habit'"
class="right-control d-flex justify-content-center pt-3"
:class="[{
'habit-control-negative-enabled': task.down && !showTaskLockIcon,
'habit-control-negative-disabled': !task.down && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.down.inner]"
'control-bottom-box': task.group.id && !isOpenTask,
'control-top-box': approvalsClass}, controlClass.down.bg]"
>
<div
class="task-control habit-control"
:class="[{
'habit-control-negative-enabled': task.down && !showTaskLockIcon,
'habit-control-negative-disabled': !task.down && !showTaskLockIcon,
'task-not-scoreable': showTaskLockIcon,
}, controlClass.down.inner]"
tabindex="0"
role="button"
:aria-label="$t('scoreDown')"
:aria-disabled="showTaskLockIcon || (!task.down && !showTaskLockIcon)"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.down ? controlClass.down.icon : 'negative'"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon negative"
v-html="icons.negative"
></div>
</div>
</div>
<!-- Rewards right side control-->
<div
v-if="task.type === 'reward'"
class="right-control d-flex align-items-center justify-content-center reward-control"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.bg,
]"
tabindex="0"
role="button"
:aria-label="$t('scoreDown')"
:aria-disabled="showTaskLockIcon || (!task.down && !showTaskLockIcon)"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
v-if="showTaskLockIcon"
class="svg-icon lock"
:class="task.down ? controlClass.down.icon : 'negative'"
class="svg-icon color lock"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon negative"
v-html="icons.negative"
class="svg-icon mb-1"
v-html="icons.gold"
></div>
<div class="small-text">
{{ task.value }}
</div>
</div>
</div>
<!-- Rewards right side control-->
<div
v-if="task.type === 'reward'"
class="right-control d-flex align-items-center justify-content-center reward-control"
:class="[
{ 'task-not-scoreable': showTaskLockIcon },
controlClass.bg,
]"
tabindex="0"
@click="score('down')"
@keypress.enter="score('down')"
>
<div
v-if="showTaskLockIcon"
class="svg-icon color lock"
v-html="icons.lock"
></div>
<div
v-else
class="svg-icon mb-1"
v-html="icons.gold"
></div>
<div class="small-text">
{{ task.value }}
</div>
</div>
<approval-footer
v-if="task.group.id && !isOpenTask"
:task="task"
:group="group"
/>
</div>
<approval-footer
v-if="task.group.id && !isOpenTask"
:task="task"
:group="group"
/>
</div>
</div>
</template>
@@ -1177,9 +1177,16 @@ export default {
moveToBottom () {
this.$emit('moveTo', this.task, 'bottom');
},
destroy () {
async destroy () {
const type = this.$t(this.task.type);
if (!window.confirm(this.$t('sureDeleteType', { type }))) return; // eslint-disable-line no-alert
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDeleteType', { type }),
taskType: type,
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.task);
this.$emit('taskDestroyed', this.task);
},

File diff suppressed because it is too large Load Diff

View File

@@ -25,8 +25,8 @@
type="checkbox"
:checked="isChecked"
:value="value"
@change="handleChange"
:disabled="disabled"
@change="handleChange"
>
<label
class="toggle-switch-label"

View File

@@ -39,7 +39,15 @@ export default {
};
const purchaseForKey = currencyToPurchaseForKey[currency];
return window.confirm(this.$t(purchaseForKey, { cost })); // eslint-disable-line no-alert
return new Promise(resolve => {
this.$root.$emit('habitica:purchase-confirm', {
message: this.$t(purchaseForKey, { cost }),
currency,
cost,
resolve,
});
});
},
},
};

View File

@@ -1,7 +1,8 @@
<template>
<tr>
<td colspan="3"
<td
v-if="!mixinData.inlineSettingMixin.modalVisible"
colspan="3"
>
<div class="d-flex justify-content-between align-items-center">
<h3
@@ -18,8 +19,9 @@
</a>
</div>
</td>
<td colspan="3"
<td
v-if="mixinData.inlineSettingMixin.modalVisible"
colspan="3"
>
<h3
v-once
@@ -59,8 +61,8 @@
{{ $t('performanceAnalytics') }}
</label>
<toggle-switch
class="mb-auto"
v-model="user.preferences.analyticsConsent"
class="mb-auto"
@change="prefToggled()"
/>
</div>
@@ -151,14 +153,14 @@ import { mapState } from '@/libs/store';
import alert from '@/assets/svg/for-css/alert.svg?raw';
export default {
mixins: [
GenericUserPreferencesMixin,
InlineSettingMixin,
],
components: {
SaveCancelButtons,
ToggleSwitch,
},
mixins: [
GenericUserPreferencesMixin,
InlineSettingMixin,
],
data () {
return {
icons: Object.freeze({

View File

@@ -14,6 +14,8 @@
<bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<purchase-confirm-modal v-if="isUserLoaded" />
<delete-task-confirm-modal v-if="isUserLoaded" />
<template v-if="isUserLoaded">
<privacy-banner />
<chat-banner />
@@ -138,6 +140,8 @@ import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
import purchaseConfirmModal from '@/components/shops/purchaseConfirmModal.vue';
import deleteTaskConfirmModal from '@/components/tasks/deleteTaskConfirmModal.vue';
import spellsMixin from '@/mixins/spells';
import {
@@ -172,6 +176,8 @@ export default {
bugReportModal,
bugReportSuccessModal,
externalLinkModal,
purchaseConfirmModal,
deleteTaskConfirmModal,
},
mixins: [notifications, spellsMixin],
data () {

View File

@@ -2,12 +2,15 @@
"challenge": "Challenge",
"challengeDetails": "Challenges are community events in which players compete and earn prizes by completing a group of related tasks.",
"brokenChaLink": "Broken Challenge Link",
"brokenTask": "Broken Challenge Link: this task was part of a challenge, but has been removed from it. What would you like to do?",
"brokenTask": "Broken Challenge Link",
"brokenTaskDescription": "This task was part of a challenge, but has been removed from it. What would you like to do?",
"keepIt": "Keep It",
"removeIt": "Remove It",
"removeTasks": "Remove Tasks",
"brokenChallenge": "Broken Challenge Link: this task was part of a challenge, but the challenge (or group) has been deleted. What to do with the orphan tasks?",
"challengeCompleted": "This challenge has been completed, and the winner was <span class=\"badge\"><%- user %></span>! What to do with the orphan tasks?",
"brokenChallenge": "Broken Challenge Link",
"brokenChallengeDescription": "This task was part of a challenge, but the challenge (or group) has been deleted. What to do with the orphan tasks?",
"challengeCompleted": "Challenge Completed!",
"challengeCompletedDescription": "The winner was <%- user %>! What to do with the orphan tasks?",
"unsubChallenge": "Broken Challenge Link: this task was part of a challenge, but you have unsubscribed from the challenge. What to do with the orphan tasks?",
"challenges": "Challenges",
"endDate": "Ends",

View File

@@ -242,5 +242,6 @@
"whyReportingPlayerPlaceholder": "Reason for report",
"playerReportModalBody": "You should only report a player who violates the <%= firstLinkStart %>Community Guidelines<%= linkEnd %> and/or <%= secondLinkStart %>Terms of Service<%= linkEnd %>. Submitting a false report is a violation of Habiticas Community Guidelines.",
"targetUserNotExist": "Target User: '<%= userName %>' does not exist.",
"rememberToBeKind": "Please remember to be kind, respectful, and follow the <a href='/static/community-guidelines' target='_blank'>Community Guidelines</a>."
"rememberToBeKind": "Please remember to be kind, respectful, and follow the <a href='/static/community-guidelines' target='_blank'>Community Guidelines</a>.",
"confirmPurchase": "Confirm Purchase"
}

View File

@@ -89,7 +89,7 @@
"fortify": "Fortify",
"fortifyComplete": "Fortify complete!",
"deleteTaskType": "Delete this <%= type %>",
"sureDeleteType": "Are you sure you want to delete this <%= type %>?",
"sureDeleteType": "Are you sure you want to delete this task?",
"streakCoins": "Streak Bonus!",
"taskToTop": "To top",
"taskToBottom": "To bottom",
@@ -138,5 +138,10 @@
"pressEnterToAddTag": "Press Enter to add tag: '<%= tagName %>'",
"taskSummary": "<%= type %> Summary",
"scoreUp": "Score up",
"scoreDown": "Score down"
"scoreDown": "Score down",
"deleteType": "Delete <%= type %>",
"deleteTask": "Delete Task",
"deleteXTasks": "Delete <%= count %> Tasks",
"brokenChallengeTaskCount": "This is one of <%= count %> tasks that are part of a Challenge that no longer exists.",
"confirmDeleteTasks": "Would you like to delete the tasks?"
}