PronounsPage/pages/names.vue
2024-09-12 10:11:25 +02:00

249 lines
10 KiB
Vue

<template>
<Page>
<NotFound v-if="!config.names || !config.names.enabled || !config.names.published && !$isGranted('names')" />
<div v-else>
<CommunityNav />
<h2>
<Icon v="signature" />
<T>names.headerLong</T>
</h2>
<section>
<T>names.intro</T>
<NamesLinks />
</section>
<section v-if="$isGranted('names')" class="px-3">
<div class="alert alert-info">
<strong>{{ namesCountApproved() }}</strong> <T>nouns.approved</T>,
<strong>{{ namesCountPending() }}</strong> <T>nouns.pending</T>.
</div>
</section>
<section class="sticky-top">
<div class="input-group mb-3 bg-white">
<span class="input-group-text">
<Icon v="filter" />
</span>
<input ref="filter" v-model="filter" class="form-control border-primary" :placeholder="$t('crud.filterLong')">
<button v-if="filter" class="btn btn-outline-danger" @click="filter = ''; $refs.filter.focus()">
<Icon v="times" />
</button>
<button class="btn btn-success" @click="$refs.form.$el.scrollIntoView({ block: 'center' })">
<Icon v="plus-circle" />
<T>nouns.submit.action</T>
</button>
</div>
</section>
<section>
<Loading :value="namesAsyncData.data.value">
<ul class="list-group small">
<template v-if="visibleNames().length">
<li v-for="name in visibleNames()" :class="['list-group-item', name.approved ? '' : 'marked']">
<Name :name="name">
<ul class="list-inline small">
<template v-if="$isGranted('names')">
<li v-if="name.author" class="list-inline-item small">
<nuxt-link :to="`/@${name.author}`" class="btn btn-outline-dark btn-sm">
<Icon v="user" />
<span class="btn-label">
<T>crud.author</T><T>quotation.colon</T>
@{{ name.author }}
</span>
</nuxt-link>
</li>
<li v-if="!name.approved" class="list-inline-item">
<button class="btn btn-success btn-sm" @click="approve(name)">
<Icon v="check" />
<span class="btn-label"><T>crud.approve</T></span>
</button>
</li>
<li v-else class="list-inline-item" @click="hide(name)">
<button class="btn btn-outline-secondary btn-sm">
<Icon v="times" />
<span class="btn-label"><T>crud.hide</T></span>
</button>
</li>
<li class="list-inline-item">
<button class="btn btn-outline-danger btn-sm" @click="remove(name)">
<Icon v="trash" />
<span class="btn-label"><T>crud.remove</T></span>
</button>
</li>
</template>
<li class="list-inline-item">
<button class="btn btn-outline-primary btn-sm" @click="edit(name)">
<Icon v="pen" />
<span class="btn-label">
<T v-if="$isGranted('names')">crud.edit</T>
<T v-else>nouns.edit</T>
</span>
</button>
</li>
</ul>
</Name>
<small v-if="name.base && names[name.base]">
<hr>
<p><strong><T>nouns.edited</T><T>quotation.colon</T></strong></p>
<Diff switchable>
<template #before><Name :name="names[name.base]" /></template>
<template #after><Name :name="name" /></template>
</Diff>
</small>
</li>
</template>
<template v-else>
<li class="list-group-item text-center">
<Icon v="search" />
<T>names.empty</T>
</li>
</template>
</ul>
</Loading>
</section>
<Separator icon="plus" />
<section class="px-3">
<NameSubmitForm ref="form" style="scroll-padding-top: 2rem;" />
</section>
</div>
</Page>
</template>
<script>
import { defineComponent } from 'vue';
import { useNuxtApp } from 'nuxt/app';
import { buildDict } from '../src/helpers.ts';
import { Name } from '../src/classes.ts';
import useConfig from '../composables/useConfig.ts';
import useDialogue from '../composables/useDialogue.ts';
import useHash from '../composables/useHash.ts';
import useSimpleHead from '../composables/useSimpleHead.ts';
export default defineComponent({
setup() {
definePageMeta({
translatedPaths: (config) => translatedPathByConfigModule(config.names),
});
const { $translator: translator } = useNuxtApp();
useSimpleHead({
title: translator.translate('names.headerLong'),
description: translator.translate('names.description'),
}, translator);
const { handleHash, setHash } = useHash();
const namesAsyncData = useAsyncData(async () => {
const namesRaw = await $fetch('/api/names');
return buildDict(function* () {
const sorted = namesRaw.sort((a, b) => {
if (a.approved && !b.approved) {
return 1;
}
if (!a.approved && b.approved) {
return -1;
}
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
for (const n of sorted) {
yield [n.id, new Name(n)];
}
});
}, {
server: false,
});
return {
config: useConfig(),
dialogue: useDialogue(),
handleHash,
setHash,
namesAsyncData,
};
},
data() {
return {
filter: '',
};
},
computed: {
names() {
if (this.namesAsyncData.status.value !== 'success') {
return {};
}
return this.namesAsyncData.data.value;
},
},
watch: {
filter() {
this.setHash('', this.filter);
},
},
async mounted() {
this.handleHash('', (filter) => {
this.filter = filter;
if (filter) {
this.$refs.filter.focus();
this.$refs.filter.scrollIntoView();
setTimeout((_) => {
this.$refs.filter.scrollIntoView();
}, 1000);
}
});
},
methods: {
visibleNames() {
return Object.values(this.names).filter((n) => n.matches(this.filter));
},
edit(name) {
this.$refs.form.edit(name);
},
async approve(name) {
await this.dialogue.postWithAlertOnError(`/api/names/approve/${name.id}`);
if (name.base) {
delete this.names[name.base];
}
name.approved = true;
name.base = null;
},
async hide(name) {
await this.dialogue.postWithAlertOnError(`/api/names/hide/${name.id}`);
name.approved = false;
},
async remove(name) {
await this.dialogue.confirm(this.$t('crud.removeConfirm'), 'danger');
await this.dialogue.postWithAlertOnError(`/api/names/remove/${name.id}`);
delete this.names[name.id];
},
// those must be methods, not computed, because when modified, they don't get updated in the view for some reason
namesCountApproved() {
return Object.values(this.names).filter((n) => n.approved).length;
},
namesCountPending() {
return Object.values(this.names).filter((n) => !n.approved).length;
},
},
});
</script>
<style lang="scss">
@import "assets/variables";
@include media-breakpoint-up('md') {
.w-md-50 {
width: 50%;
}
}
.list-group-item.marked {
border-inline-start: 3px solid $primary;
}
</style>