mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-16 17:38:21 -04:00
392 lines
17 KiB
Vue
392 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import type { ComponentExposed } from 'vue-component-type-helpers';
|
|
|
|
import type InclusiveSubmitForm from '~/components/InclusiveSubmitForm.vue';
|
|
import type Table from '~/components/Table.vue';
|
|
import { InclusiveEntry } from '~/src/classes.ts';
|
|
import { buildDict, clearUrl, 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 InclusiveSubmitForm>>('form');
|
|
|
|
const entriesAsyncData = useAsyncData(async () => {
|
|
const entriesRaw = await $fetch('/api/inclusive');
|
|
|
|
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.insteadOf.toLowerCase()).localeCompare(clearLinkedText(b.insteadOf.toLowerCase()));
|
|
});
|
|
for (const w of sorted) {
|
|
yield [w.id, new InclusiveEntry(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(() => {
|
|
if (entriesAsyncData.status.value !== 'success') {
|
|
return {};
|
|
}
|
|
return entriesAsyncData.data.value!;
|
|
});
|
|
const visibleEntries = computed(() => {
|
|
return Object.values(entries.value).filter((entry) => entry.matches(filter.value));
|
|
});
|
|
|
|
const categoriesTexts = Object.fromEntries(config.inclusive.categories?.map((category) => {
|
|
return [category.key, category.text];
|
|
}) ?? []);
|
|
|
|
const dialogue = useDialogue();
|
|
const edit = (entry: InclusiveEntry) => {
|
|
form.value?.edit(entry);
|
|
};
|
|
const approve = async (entry: InclusiveEntry): Promise<void> => {
|
|
await dialogue.postWithAlertOnError(`/api/inclusive/approve/${entry.id}`);
|
|
if (entry.base) {
|
|
delete entries.value[entry.base];
|
|
}
|
|
entry.approved = true;
|
|
entry.base = null;
|
|
};
|
|
const hide = async (entry: InclusiveEntry): Promise<void> => {
|
|
await dialogue.postWithAlertOnError(`/api/inclusive/hide/${entry.id}`);
|
|
entry.approved = false;
|
|
};
|
|
const remove = async (entry: InclusiveEntry): Promise<void> => {
|
|
await dialogue.confirm(translator.translate('crud.removeConfirm'), 'danger');
|
|
|
|
await dialogue.postWithAlertOnError(`/api/inclusive/remove/${entry.id}`);
|
|
delete entries.value[entry.id];
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Loading :value="entriesAsyncData.data.value">
|
|
<ModerationSection
|
|
v-model="filter.moderation"
|
|
kind="inclusive"
|
|
:moderation-filters="['unapproved', 'no category']"
|
|
:entries="Object.values(entries)"
|
|
/>
|
|
|
|
<FilterBar
|
|
v-model="filter.text"
|
|
v-model:category="filter.category"
|
|
:categories="config.inclusive.categories"
|
|
submit-button
|
|
@submit-clicked="form?.focus()"
|
|
/>
|
|
|
|
<Table ref="dictionarytable" :data="visibleEntries" :marked="(el) => !el.approved" fixed>
|
|
<template #header>
|
|
<div class="bold text-nowrap">
|
|
<Icon v="comment-times" />
|
|
<T>inclusive.insteadOf</T>
|
|
</div>
|
|
<div class="bold text-nowrap">
|
|
<Icon v="comment-check" />
|
|
<T>inclusive.say</T>
|
|
</div>
|
|
<div class="d-none d-md-block bold text-nowrap">
|
|
<Icon v="comment-dots" />
|
|
<T>inclusive.because</T>
|
|
</div>
|
|
</template>
|
|
|
|
<template #row="s">
|
|
<template v-if="s">
|
|
<div style="grid-area: insteadOf">
|
|
<ul class="mb-0 ps-4">
|
|
<li v-for="(w, i) in s.el.insteadOf" :key="i" class="text-strike">
|
|
<LinkedText :text="w" noicons />
|
|
</li>
|
|
</ul>
|
|
|
|
<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>
|
|
<ul class="mb-0 ps-4">
|
|
<li v-for="w in entries[s.el.base].insteadOf" :key="w" class="text-strike">
|
|
<LinkedText :text="w" noicons />
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
<template #after>
|
|
<ul class="mb-0 ps-4">
|
|
<li v-for="(w, i) in s.el.insteadOf" :key="i" class="text-strike">
|
|
<LinkedText :text="w" noicons />
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</Diff>
|
|
</div>
|
|
</div>
|
|
<div style="grid-area: say">
|
|
<ul class="mb-0 ps-4">
|
|
<li v-for="(w, i) in s.el.say" :key="i">
|
|
<LinkedText :text="w" noicons />
|
|
</li>
|
|
</ul>
|
|
|
|
<small v-if="s.el.base && entries[s.el.base]">
|
|
<p><strong><T>nouns.edited</T><T>quotation.colon</T></strong></p>
|
|
|
|
<Diff switchable>
|
|
<template #before>
|
|
<ul class="mb-0 ps-4">
|
|
<li v-for="(w, i) in entries[s.el.base].say" :key="i">
|
|
<LinkedText :text="w" noicons />
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
<template #after>
|
|
<ul class="mb-0 ps-4">
|
|
<li v-for="(w, i) in s.el.say" :key="i">
|
|
<LinkedText :text="w" noicons />
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</Diff>
|
|
</small>
|
|
</div>
|
|
<div style="grid-area: because">
|
|
<p v-for="(p, i) in s.el.because.split('\n\n')" :key="i">
|
|
<LinkedText :text="p" noicons />
|
|
</p>
|
|
|
|
<ul class="list-unstyled small">
|
|
<li v-for="(link, i) in s.el.links" :key="i">
|
|
<a :href="link" target="_blank" rel="noopener">
|
|
<Icon v="external-link" />
|
|
{{ clearUrl(link) }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<small v-if="s.el.base && entries[s.el.base]">
|
|
<p><strong><T>nouns.edited</T><T>quotation.colon</T></strong></p>
|
|
|
|
<Diff switchable>
|
|
<template #before>
|
|
<p v-for="(p, i) in entries[s.el.base].because.split('\n\n')" :key="i">
|
|
<LinkedText :text="p" noicons />
|
|
</p>
|
|
|
|
<ul class="list-unstyled small">
|
|
<li v-for="(link, i) in entries[s.el.base].links" :key="i">
|
|
<a :href="link" target="_blank" rel="noopener">
|
|
<Icon v="external-link" />
|
|
{{ clearUrl(link) }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
<template #after>
|
|
<p v-for="(p, i) in s.el.because.split('\n\n')" :key="i">
|
|
<LinkedText :text="p" noicons />
|
|
</p>
|
|
|
|
<ul class="list-unstyled small">
|
|
<li v-for="(link, i) in s.el.links" :key="i">
|
|
<a :href="link" target="_blank" rel="noopener">
|
|
<Icon v="external-link" />
|
|
{{ clearUrl(link) }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</Diff>
|
|
</small>
|
|
</div>
|
|
<div
|
|
v-if="s.el.clarification"
|
|
class="alert alert-warning mb-0 px-2 py-1 small"
|
|
style="grid-area: clarification"
|
|
>
|
|
<LinkedText :text="s.el.clarification" />
|
|
</div>
|
|
<div style="grid-area: categories">
|
|
<ul class="list-inline mb-0" style="grid-area: categories">
|
|
<li v-for="category in s.el.categories" :key="category" class="list-inline-item">
|
|
<a
|
|
:href="`#:${category}`"
|
|
class="badge bg-primary text-white"
|
|
@click.prevent="filter.category = category"
|
|
>
|
|
{{ categoriesTexts[category] }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<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>
|
|
<ul class="list-inline">
|
|
<li
|
|
v-for="category in entries[s.el.base].categories"
|
|
:key="category"
|
|
class="list-inline-item"
|
|
>
|
|
<span class="badge bg-primary text-white">
|
|
{{ categoriesTexts[category] }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
<template #after>
|
|
<ul class="list-inline">
|
|
<li
|
|
v-for="category in s.el.categories"
|
|
:key="category"
|
|
class="list-inline-item"
|
|
>
|
|
<span class="badge bg-primary text-white">
|
|
{{ categoriesTexts[category] }}
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</Diff>
|
|
</div>
|
|
</div>
|
|
<div style="grid-area: buttons">
|
|
<ul class="d-flex flex-wrap flex-md-column list-unstyled list-btn-concise mb-0">
|
|
<template v-if="$isGranted('inclusive')">
|
|
<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
|
|
type="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>
|
|
<button
|
|
type="button"
|
|
class="btn btn-concise btn-outline-secondary btn-sm m-1"
|
|
@click="hide(s.el)"
|
|
>
|
|
<Icon v="times" />
|
|
<span class="btn-label"><T>crud.hide</T></span>
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button
|
|
type="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
|
|
type="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('inclusive')">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" />
|
|
|
|
<InclusiveSubmitForm ref="form" @submit="reloadEntries()" />
|
|
</Loading>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
@import "assets/variables";
|
|
|
|
:deep(.row-header) {
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
@include media-breakpoint-up('md', $grid-breakpoints) {
|
|
grid-template-columns: 1fr 1fr 2.5fr 3em;
|
|
}
|
|
}
|
|
|
|
:deep(.row-content) {
|
|
grid:
|
|
"insteadOf say"
|
|
"because because"
|
|
"clarification clarification"
|
|
"categories categories"
|
|
"buttons buttons"
|
|
/ 1fr 1fr;
|
|
|
|
@include media-breakpoint-up('md', $grid-breakpoints) {
|
|
grid:
|
|
"insteadOf say because buttons"
|
|
"clarification clarification because buttons"
|
|
"categories categories because buttons" 1fr
|
|
/ 1fr 1fr 2.5fr 3em;
|
|
}
|
|
}
|
|
</style>
|