Replace browser confirmations with confirmation modals (#15544)

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* Confirmation prompts (replace browser confirmation prompts)

* lint

* lint

* Fix confirmation modal not showing for items w/gems

* Updated delete and confirmation modals

* lint

* Fix top bar on color on confirmation prompt clipping

* Show currency amount on purchase confirmation modals

* confirmation modals using existing components

* lint

* Broken task confirmation modal

* Replace class change confirmation with modal

* fix(style): correct indentation

* Replace warning svg w/ existing alert svg, and reset language strings

* fix(json): json doesn't like comma dangles

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
This commit is contained in:
Fiz
2026-01-06 15:19:30 -06:00
committed by GitHub
parent d87946d912
commit 781a904583
74 changed files with 762 additions and 152 deletions
@@ -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.alertIcon"
></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,175 @@
</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;
path {
fill: #DE3F3F;
}
}
}
.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 alertIcon from '@/assets/svg/for-css/alert.svg?raw';
export default {
mixins: [notifications],
data () {
return {
brokenChallengeTask: {},
icons: Object.freeze({
alertIcon,
}),
};
},
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 +231,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 +282,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();
},