Files
habitica/website/client/src/components/admin/admin-panel/user-support/customizationsOwned.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

253 lines
6.3 KiB
Vue

<template>
<div class="card mt-2">
<div class="card-header">
<h3
class="mb-0 mt-0"
:class="{'open': expand}"
@click="expand = !expand"
>
Customizations
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div
v-for="itemType in itemTypes"
:key="itemType"
>
<div
v-if="collatedItemData[itemType]"
class="accordion-group"
>
<h4
class="expand-toggle"
:class="{'open': expandItemType[itemType]}"
@click="expandItemType[itemType] = !expandItemType[itemType]"
>
{{ itemType }}
</h4>
<div v-if="expandItemType[itemType]">
<ul>
<li
v-for="item in collatedItemData[itemType]"
:key="item.path"
>
<form @submit.prevent="saveItem(item)">
<span
class="enableValueChange"
@click="enableValueChange(item)"
>
<span :class="item.value ? 'owned' : 'not-owned'">
{{ item.value }}
</span>
:
<span :class="{ ownedItem: !item.neverOwned }">{{ item.text }}</span>
</span>
- {{ itemType }}.{{ item.key }} - <i> {{ item.set }}</i>
<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;
}
.owned {
color: green;
}
.not-owned {
color: red;
}
</style>
<script>
import content from '@/../../common/script/content';
import getItemDescription from '../mixins/getItemDescription';
import saveHero from '../mixins/saveHero';
const months = [
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December',
];
function makeSetText (set) {
if (set === undefined) {
return '';
}
if (set.key.indexOf('backgrounds') === 0) {
const { text } = set;
return `${months[parseInt(text.slice(11, 13), 10) - 1]} ${text.slice(13)}`;
}
return set.key;
}
function collateItemData (self) {
const collatedItemData = {};
self.itemTypes.forEach(itemType => {
// itemTypes are pets, food, gear, etc
// Set up some basic data for this itemType:
const basePath = `purchased.${itemType}`;
let ownedItems;
let allItems;
if (itemType.indexOf('hair') === 0) {
const hairType = itemType.split('.')[1];
allItems = content.appearances.hair[hairType];
if (self.hero.purchased && self.hero.purchased.hair) {
ownedItems = self.hero.purchased.hair[hairType] || {};
} else {
ownedItems = {};
}
} else {
allItems = content.appearances[itemType];
ownedItems = self.hero.purchased[itemType] || {};
}
const itemData = []; // all items for this itemType
// Collate data for items that the user owns or used to own:
for (const key of Object.keys(ownedItems)) {
// Do not sort keys. The order in the items object gives hints about order received.
const item = allItems[key];
itemData.push({
itemType,
key,
text: item.text ? item.text() : key,
modified: false,
path: `${basePath}.${key}`,
value: ownedItems[key],
set: makeSetText(item.set),
});
}
// Collate data for items that the user never owned:
for (const key of Object.keys(allItems).sort()) {
if (
// ignore items the user owns because we captured them above:
!(key in ownedItems)
) {
const item = allItems[key];
itemData.push({
itemType,
key,
text: item.text ? item.text() : key,
modified: false,
path: `${basePath}.${key}`,
value: false,
set: makeSetText(item.set),
});
}
}
if (itemData.length > 0) {
collatedItemData[itemType] = itemData;
}
});
return collatedItemData;
}
function resetData (self) {
self.collatedItemData = collateItemData(self);
self.itemTypes.forEach(itemType => { self.expandItemType[itemType] = false; });
}
export default {
mixins: [
getItemDescription,
saveHero,
],
props: {
resetCounter: {
type: Number,
required: true,
},
hero: {
type: Object,
required: true,
},
},
data () {
return {
expand: false,
expandItemType: {
skin: false,
shirt: false,
background: false,
'hair.bangs': false,
'hair.base': false,
'hair.color': false,
'hair.mustache': false,
'hair.beard': false,
'hair.flower': false,
},
itemTypes: ['skin', 'shirt', 'background', 'hair.bangs', 'hair.base', 'hair.color', 'hair.mustache', 'hair.beard', 'hair.flower'],
nonIntegerTypes: ['skin', 'shirt', 'background'],
collatedItemData: {},
};
},
watch: {
resetCounter () {
resetData(this);
},
},
mounted () {
resetData(this);
},
methods: {
async saveItem (item) {
await this.saveHero({
hero: {
_id: this.hero._id,
purchasedPath: item.path,
purchasedVal: item.value,
},
msg: item.path,
});
item.modified = false;
},
enableValueChange (item) {
// allow form field(s) to be shown:
item.modified = true;
item.value = !item.value;
},
},
};
</script>