mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d9cbaa42a | ||
|
|
81b7af35ea | ||
|
|
4468f06074 | ||
|
|
2948824df7 | ||
|
|
de1b509243 | ||
|
|
fd0dedf72e | ||
|
|
8da456f4b7 | ||
|
|
a22691d11f | ||
|
|
1045a17354 | ||
|
|
bf1ea90720 | ||
|
|
ccc7e7d7a7 | ||
|
|
270ff2e034 | ||
|
|
560dc5a896 | ||
|
|
c0d36db6af | ||
|
|
80de056bab | ||
|
|
b154fe2564 | ||
|
|
a4a8dacc31 | ||
|
|
c60822f44f |
10
website/client/src/assets/svg/warning_icon.svg
Normal file
10
website/client/src/assets/svg/warning_icon.svg
Normal 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 |
@@ -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"
|
||||
|
||||
@@ -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':
|
||||
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}`, () => {
|
||||
|
||||
@@ -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,17 +873,25 @@ 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)
|
||||
) {
|
||||
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);
|
||||
if (this.item.key !== 'rebirth_orb') {
|
||||
await this.purchased(this.item.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('buyPressed', this.item);
|
||||
this.hideDialog();
|
||||
|
||||
207
website/client/src/components/shops/purchaseConfirmModal.vue
Normal file
207
website/client/src/components/shops/purchaseConfirmModal.vue
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
@@ -64,9 +64,11 @@
|
||||
<li>sexual orientation; and</li>
|
||||
<li>information collected from a known child.</li>
|
||||
</ul>
|
||||
<p><strong>
|
||||
<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>
|
||||
</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 children’s 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 children’s 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 class="top-bar"></div>
|
||||
<div class="modal-body-content">
|
||||
<div
|
||||
v-if="brokenChallengeTask.challenge.broken === 'TASK_DELETED'
|
||||
|| brokenChallengeTask.challenge.broken === 'CHALLENGE_TASK_NOT_FOUND'"
|
||||
>
|
||||
<h2>{{ $t('brokenTask') }}</h2>
|
||||
<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>
|
||||
<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-bottom: 2em;
|
||||
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();
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
|
||||
219
website/client/src/components/tasks/deleteTaskConfirmModal.vue
Normal file
219
website/client/src/components/tasks/deleteTaskConfirmModal.vue
Normal 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>
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -1305,9 +1305,16 @@ export default {
|
||||
}
|
||||
this.$root.$emit('bv::hide::modal', 'task-modal');
|
||||
},
|
||||
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);
|
||||
this.$root.$emit('bv::hide::modal', 'task-modal');
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
:value="value"
|
||||
@change="handleChange"
|
||||
:disabled="disabled"
|
||||
@change="handleChange"
|
||||
>
|
||||
<label
|
||||
class="toggle-switch-label"
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 Habitica’s 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"
|
||||
}
|
||||
|
||||
@@ -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?"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user