PronounsPage/components/TermsDictionary.vue
2025-02-15 14:27:00 +01:00

239 lines
8.7 KiB
Vue

<script setup lang="ts">
import type { ComponentExposed } from 'vue-component-type-helpers';
import type Table from '~/components/Table.vue';
import type TermsSubmitForm from '~/components/TermsSubmitForm.vue';
import { TermsEntry } from '~/src/classes.ts';
import type { TermsEntryRaw } from '~/src/classes.ts';
import { loadCalendar } from '~/src/data.ts';
import { buildDict, clearLinkedText } from '~/src/helpers.ts';
const props = defineProps<{
load?: boolean;
}>();
const { $translator: translator } = useNuxtApp();
const config = useConfig();
const filter = useFilterWithCategory();
const dictionarytable = useTemplateRef<ComponentExposed<typeof Table>>('dictionarytable');
watch(filter, () => {
if (dictionarytable.value) {
dictionarytable.value.reset();
dictionarytable.value.focus();
}
});
const form = useTemplateRef<InstanceType<typeof TermsSubmitForm>>('form');
const entriesAsyncData = useAsyncData(async () => {
const entriesRaw = await $fetch<TermsEntryRaw[]>('/api/terms');
return buildDict(function* () {
const sorted = entriesRaw.sort((a, b) => {
if (a.approved && !b.approved) {
return 1;
}
if (!a.approved && b.approved) {
return -1;
}
return clearLinkedText(a.term.toLowerCase()).localeCompare(clearLinkedText(b.term.toLowerCase()));
});
for (const w of sorted) {
yield [w.id, new TermsEntry(w)];
}
});
}, {
immediate: false,
});
onMounted(async () => {
if (props.load) {
await entriesAsyncData.execute();
}
});
const reloadEntries = async (): Promise<void> => {
await entriesAsyncData.execute();
form.value?.focus(false);
};
const entries = computed((): Record<string, TermsEntry> => {
if (entriesAsyncData.status.value !== 'success') {
return {};
}
return entriesAsyncData.data.value!;
});
const visibleEntries = computed((): TermsEntry[] => {
const values = Object.values(entries.value).filter((n) => n.matches(filter.value));
if (filter.value.text) {
return values.sort((a, b) => {
if (a.key && a.key.toLowerCase() === filter.value.text.toLowerCase()) {
return -1;
}
if (b.key && b.key.toLowerCase() === filter.value.text.toLowerCase()) {
return 1;
}
if (a.term[0].toLowerCase() === filter.value.text.toLowerCase()) {
return -1;
}
if (b.term[0].toLowerCase() === filter.value.text.toLowerCase()) {
return 1;
}
return a.term[0].localeCompare(b.term[0]);
});
}
return values;
});
const dialogue = useDialogue();
const edit = (entry: TermsEntry): void => {
form.value?.edit(entry);
};
const approve = async (entry: TermsEntry): Promise<void> => {
await dialogue.postWithAlertOnError(`/api/terms/approve/${entry.id}`);
if (entry.base) {
delete entries.value[entry.base];
}
entry.approved = true;
entry.base = null;
};
const hide = async (entry: TermsEntry): Promise<void> => {
await dialogue.postWithAlertOnError(`/api/terms/hide/${entry.id}`);
entry.approved = false;
};
const remove = async (entry: TermsEntry): Promise<void> => {
await dialogue.confirm(translator.translate('crud.removeConfirm'), 'danger');
await dialogue.postWithAlertOnError(`/api/terms/remove/${entry.id}`);
delete entries.value[entry.id];
};
const year = (await loadCalendar()).getCurrentYear()!;
</script>
<template>
<Loading :value="entriesAsyncData.data.value">
<ModerationSection
v-model="filter.moderation"
kind="terms"
:moderation-filters="['unapproved', 'no key', 'no image', 'no category']"
:entries="Object.values(entries)"
/>
<FilterBar
v-model="filter.text"
v-model:category="filter.category"
:categories="config.terminology.categories"
submit-button
@submit-clicked="form?.focus()"
/>
<Table ref="dictionarytable" :data="visibleEntries" fixed :marked="(el) => !el.approved">
<template #row="s">
<template v-if="s">
<div>
<Term
:term="s.el"
category-link
flags
versions
:events="s.el.key !== null ? year.eventsByTerm[s.el.key] : undefined"
@filter="(category) => filter = { text: '', category, moderation: undefined }"
/>
<div v-if="s.el.base && entries[s.el.base]" class="small">
<p><strong><T>nouns.edited</T><T>quotation.colon</T></strong></p>
<Diff switchable>
<template #before>
<Term :term="entries[s.el.base]" flags />
</template>
<template #after>
<Term :term="s.el" flags />
</template>
</Diff>
</div>
</div>
<div>
<ul class="d-flex flex-wrap flex-md-column list-unstyled list-btn-concise mb-0">
<template v-if="$isGranted('terms')">
<li v-if="s.el.author" class="small">
<nuxt-link :to="`/@${s.el.author}`" class="btn btn-concise btn-outline-dark btn-sm m-1">
<Icon v="user" />
<span class="btn-label">
<T>crud.author</T><T>quotation.colon</T>
@{{ s.el.author }}
</span>
</nuxt-link>
</li>
<li v-if="!s.el.approved">
<button class="btn btn-concise btn-success btn-sm m-1" @click="approve(s.el)">
<Icon v="check" />
<span class="btn-label"><T>crud.approve</T></span>
</button>
</li>
<li v-else @click="hide(s.el)">
<button class="btn btn-concise btn-outline-secondary btn-sm m-1">
<Icon v="times" />
<span class="btn-label"><T>crud.hide</T></span>
</button>
</li>
<li>
<button class="btn btn-concise btn-outline-danger btn-sm m-1" @click="remove(s.el)">
<Icon v="trash" />
<span class="btn-label"><T>crud.remove</T></span>
</button>
</li>
</template>
<li>
<button class="btn btn-concise btn-outline-primary btn-sm m-1" @click="edit(s.el)">
<Icon v="pen" />
<span class="btn-label">
<T v-if="$isGranted('terms')">crud.edit</T>
<T v-else>nouns.edit</T>
</span>
</button>
</li>
</ul>
</div>
</template>
</template>
<template #empty>
<Icon v="search" />
<T>nouns.empty</T>
</template>
</Table>
<AdPlaceholder :phkey="['content-1', 'content-mobile-1']" />
<Separator icon="plus" />
<TermsSubmitForm ref="form" @submit="reloadEntries()" />
</Loading>
</template>
<style scoped lang="scss">
@import "assets/variables";
tr {
.hover-show {
opacity: 0;
}
&:hover .hover-show {
opacity: 1;
}
}
:deep(.row-content) {
@include media-breakpoint-up('md', $grid-breakpoints) {
grid-template-columns: 1fr 3em;
}
}
@include media-breakpoint-down('md', $grid-breakpoints) {
.cell-wide {
min-width: 90vw;
}
}
</style>