Files
habitica/website/client/src/components/admin/admin-panel/user-support/achievements.vue
Phillip Thelen 12773d539e Add interface to block ip-addresses or clients due to abuse (#15484)
* Read IP blocks from database

* begin building general blocking solution

* add new frontend files

* Add UI for managing blockers

* correctly reset local data after creating blocker

* Tweak wording

* Add UI for managing blockers

* restructure admin pages

* improve test coverage

* Improve blocker UI

* add blocker to block emails from registration

* lint fix

* fix

* lint fixes

* fix import

* add new permission for managing blockers

* improve permission check

* fix managing permissions from admin

* improve navbar display for non fullAccess admin

* update block error strings

* lint fix

* add option to errorHandler to skip logging

* validate blocker value during input

* improve blocker form display

* chore(subproj): reconcile habitica-images

* fix(scripts): use same Mongo version for dev/test

* fix(whitespace): eof

* documentation improvements

* remove nconf import

* remove old test

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2025-08-06 15:08:07 -05:00

314 lines
8.7 KiB
Vue

<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Achievements
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<ul>
<li
v-for="item in achievements"
:key="item.path"
v-b-tooltip.hover="item.notes"
>
<form @submit.prevent="saveItem(item)">
<span
class="enableValueChange"
@click="enableValueChange(item)"
>
<span :class="item.value ? 'achieved' : 'not-achieved'">
{{ item.value }}
</span>
:
{{ item.text || item.key }} - <i> {{ item.key }} </i>
</span>
<div
v-if="item.modified"
class="form-inline"
>
<input
v-if="item.valueIsInteger"
v-model="item.value"
class="form-control valueField"
type="number"
>
<input
v-if="item.modified"
type="submit"
value="Save"
class="btn btn-primary"
>
</div>
</form>
</li>
</ul>
<div
v-for="achievementType in nestedAchievementKeys"
:key="achievementType"
>
<div class="accordion-group">
<h4
class="expand-toggle"
:class="{'open': expandItemType[achievementType]}"
@click="expandItemType[achievementType] = !expandItemType[achievementType]"
>
{{ achievementType }}
</h4>
<div v-if="expandItemType[achievementType]">
<ul>
<li
v-for="item in nestedAchievements[achievementType]"
:key="item.path"
v-b-tooltip.hover="item.notes"
>
<form @submit.prevent="saveItem(item)">
<span
class="enableValueChange"
@click="enableValueChange(item)"
>
<span :class="item.value ? 'achieved' : 'not-achieved'">
{{ item.value }}
</span>
:
{{ item.text || item.key }} - <i> {{ item.key }} </i>
</span>
<div
v-if="item.modified"
class="form-inline"
>
<input
v-if="item.valueIsInteger"
v-model="item.value"
class="form-control valueField"
type="number"
>
<input
v-if="item.modified"
type="submit"
value="Save"
class="btn btn-primary"
>
</div>
</form>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
ul li {
margin-bottom: 0.2em;
}
.ownedItem {
font-weight: bold;
}
.enableValueChange:hover {
text-decoration: underline;
cursor: pointer;
}
.valueField {
min-width: 10ch;
}
.achieved {
color: green;
}
.not-achieved {
color: red;
}
</style>
<script>
import content from '@/../../common/script/content';
import i18n from '@/../../common/script/i18n';
import saveHero from '../mixins/saveHero';
function getText (achievementItem) {
if (achievementItem === undefined) {
return '';
}
const { titleKey } = achievementItem;
if (titleKey !== undefined) {
return i18n.t(titleKey);
}
const { singularTitleKey } = achievementItem;
if (singularTitleKey !== undefined) {
return i18n.t(singularTitleKey);
}
return achievementItem.key;
}
function getNotes (achievementItem, count) {
if (achievementItem === undefined) {
return '';
}
const { textKey } = achievementItem;
if (textKey !== undefined) {
return i18n.t(textKey, { count });
}
const { singularTextKey } = achievementItem;
if (singularTextKey !== undefined) {
return i18n.t(singularTextKey, { count });
}
return '';
}
export default {
mixins: [
saveHero,
],
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
expandItemType: {
quests: false,
ultimateGearSets: false,
},
nestedAchievementKeys: ['quests', 'ultimateGearSets'],
integerTypes: ['streak', 'perfect', 'birthday', 'habiticaDays', 'habitSurveys', 'habitBirthdays',
'valentine', 'congrats', 'shinySeed', 'goodluck', 'thankyou', 'seafoam', 'snowball', 'quests',
'rebirths', 'rebirthLevel', 'greeting', 'spookySparkles', 'nye', 'costumeContests', 'congrats',
'getwell', 'beastMasterCount', 'mountMasterCount', 'triadBingoCount',
],
cardTypes: ['greeting', 'birthday', 'valentine', 'goodluck', 'thankyou', 'greeting', 'nye',
'congrats', 'getwell'],
achievements: [],
nestedAchievements: {},
};
},
watch: {
resetCounter () {
this.resetData();
},
},
mounted () {
this.resetData();
},
methods: {
async saveItem (item) {
await this.saveHero({
hero: {
_id: this.hero._id,
achievementPath: item.path,
achievementVal: item.value,
},
msg: item.path,
});
item.modified = false;
},
enableValueChange (item) {
// allow form field(s) to be shown:
item.modified = true;
if (!item.valueIsInteger) {
item.value = !item.value;
}
},
resetData () {
this.collateItemData();
this.nestedAchievementKeys.forEach(itemType => { this.expandItemType[itemType] = false; });
},
collateItemData () {
const achievements = [];
const nestedAchievements = {};
const basePath = 'achievements';
const ownedAchievements = this.hero.achievements;
const allAchievements = content.achievements;
const ownedKeys = Object.keys(ownedAchievements).sort();
for (const key of ownedKeys) {
const value = ownedAchievements[key];
let contentKey = key;
if (this.cardTypes.indexOf(key) !== -1) {
contentKey += 'Cards';
}
if (typeof value === 'object') {
nestedAchievements[key] = [];
for (const nestedKey of Object.keys(value)) {
const valueIsInteger = this.integerTypes.includes(key);
let text = nestedKey;
if (allAchievements[key] && allAchievements[key][contentKey]) {
text = getText(allAchievements[key][contentKey]);
}
let notes = '';
if (allAchievements[key] && allAchievements[key][contentKey]) {
notes = getNotes(allAchievements[key][contentKey], ownedAchievements[key]);
}
nestedAchievements[key].push({
key: nestedKey,
text,
notes,
achievementType: key,
modified: false,
path: `${basePath}.${key}.${nestedKey}`,
value: value[nestedKey],
valueIsInteger,
});
}
} else {
const valueIsInteger = this.integerTypes.includes(key);
achievements.push({
key,
text: getText(allAchievements[contentKey]),
notes: getNotes(allAchievements[contentKey], ownedAchievements[key]),
modified: false,
path: `${basePath}.${key}`,
value: ownedAchievements[key],
valueIsInteger,
});
}
}
const allKeys = Object.keys(allAchievements).sort();
for (const key of allKeys) {
if (key !== '' && !key.endsWith('UltimateGear') && !key.endsWith('Quest')) {
const ownedKey = key.replace('Cards', '');
if (ownedAchievements[ownedKey] === undefined) {
const valueIsInteger = this.integerTypes.includes(ownedKey);
achievements.push({
key: ownedKey,
text: getText(allAchievements[key]),
notes: getNotes(allAchievements[key], 0),
modified: false,
path: `${basePath}.${ownedKey}`,
value: valueIsInteger ? 0 : false,
valueIsInteger,
neverOwned: true,
});
}
}
}
this.achievements = achievements;
this.nestedAchievements = nestedAchievements;
},
},
};
</script>