PronounsPage/pages/names.vue
2025-03-23 11:44:39 +01:00

224 lines
8.6 KiB
Vue

<script setup lang="ts">
import { useNuxtApp } from 'nuxt/app';
import type NameSubmitForm from '~/components/NameSubmitForm.vue';
import useConfig from '~/composables/useConfig.ts';
import useDialogue from '~/composables/useDialogue.ts';
import useHash from '~/composables/useHash.ts';
import useSimpleHead from '~/composables/useSimpleHead.ts';
import { Name } from '~/src/classes.ts';
import { buildDict } from '~/src/helpers.ts';
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,
});
const config = useConfig();
const filter = ref('');
watch(filter, () => {
setHash('', filter.value);
});
const names = computed(() => {
if (namesAsyncData.status.value !== 'success') {
return {};
}
return namesAsyncData.data.value!;
});
const filterBar = useTemplateRef('filterBar');
onMounted(async () => {
handleHash('', (hash) => {
filter.value = hash;
if (hash) {
filterBar.value?.focus();
}
});
});
const visibleNames = computed(() => {
return Object.values(names.value).filter((n) => n.matches(filter.value));
});
const form = useTemplateRef<InstanceType<typeof NameSubmitForm>>('form');
const edit = (name: Name) => {
form.value?.edit(name);
};
const dialogue = useDialogue();
const approve = async (name: Name) => {
await dialogue.postWithAlertOnError(`/api/names/approve/${name.id}`);
if (name.base) {
delete names.value[name.base];
}
name.approved = true;
name.base = null;
};
const hide = async (name: Name) => {
await dialogue.postWithAlertOnError(`/api/names/hide/${name.id}`);
name.approved = false;
};
const remove = async (name: Name) => {
await dialogue.confirm(translator.translate('crud.removeConfirm'), 'danger');
await dialogue.postWithAlertOnError(`/api/names/remove/${name.id}`);
delete names.value[name.id];
};
// those must be methods, not computed, because when modified, they don't get updated in the view for some reason
const namesCountApproved = () => {
return Object.values(names.value).filter((n) => n.approved).length;
};
const namesCountPending = () => {
return Object.values(names.value).filter((n) => !n.approved).length;
};
</script>
<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>
<FilterBar
ref="filterBar"
v-model="filter"
submit-button
@submit-clicked="form?.focus()"
/>
<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']">
<NamesEntry :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>
</NamesEntry>
<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><NamesEntry :name="names[name.base]" /></template>
<template #after><NamesEntry :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" />
<NameSubmitForm ref="form" />
</div>
</Page>
</template>
<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>