mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-25 14:09:03 -04:00
370 lines
17 KiB
Vue
370 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import { useNuxtApp } from 'nuxt/app';
|
|
|
|
import buildLocaleList from '#shared/buildLocaleList.ts';
|
|
import { longtimeCookieSetting } from '#shared/cookieSettings.ts';
|
|
import { formatFonts } from '#shared/fonts.ts';
|
|
import { newDate, PermissionAreas } from '#shared/helpers.ts';
|
|
import useSimpleHead from '~/composables/useSimpleHead.ts';
|
|
import { loadConfig } from '~/src/data.ts';
|
|
import { useMainStore } from '~/store/index.ts';
|
|
import type { Config } from '~~/locale/config.ts';
|
|
import type { LocaleDescription } from '~~/locale/locales.ts';
|
|
|
|
const { $translator: translator, $isGranted: isGranted } = useNuxtApp();
|
|
useSimpleHead({
|
|
title: translator.translate('admin.header'),
|
|
}, translator);
|
|
|
|
const config = useConfig();
|
|
|
|
const tokenCookie = useCookie('token', longtimeCookieSetting);
|
|
const impersonatorCookie = useCookie('impersonator', longtimeCookieSetting);
|
|
|
|
const { data: stats } = await useFetch('/api/admin/stats');
|
|
|
|
type LocalDescriptionWithConfig = Omit<LocaleDescription, 'matches' | 'fullName'> & { config: Config };
|
|
const getVisibleLocales = async (): Promise<LocalDescriptionWithConfig[]> => {
|
|
const locales = buildLocaleList(config.locale, true);
|
|
return await Promise.all(locales
|
|
.filter((localeDescription) => {
|
|
return isGranted(PermissionAreas.Panel, localeDescription.code) &&
|
|
stats.value?.locales[localeDescription.code];
|
|
})
|
|
.map(async (localeDescription) => ({
|
|
...localeDescription,
|
|
config: await loadConfig(localeDescription.code),
|
|
})));
|
|
};
|
|
|
|
const visibleLocales = await getVisibleLocales();
|
|
|
|
const store = useMainStore();
|
|
|
|
const adminNotifications = ref(store.user ? store.user.adminNotifications ?? 7 : 7);
|
|
watch(adminNotifications, async () => {
|
|
const res = await $fetch('/api/admin/set-notification-frequency', {
|
|
method: 'POST',
|
|
body: { frequency: adminNotifications.value },
|
|
});
|
|
await store.setToken(res.token);
|
|
});
|
|
|
|
const filterAttention = ref<boolean>(false);
|
|
|
|
const router = useRouter();
|
|
const impersonate = async (email: string) => {
|
|
const { token } = await $fetch(`/api/admin/impersonate/${encodeURIComponent(email)}`);
|
|
impersonatorCookie.value = tokenCookie.value;
|
|
tokenCookie.value = token;
|
|
await router.push({ name: 'user' });
|
|
setTimeout(() => window.location.reload(), 500);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Page wide>
|
|
<NotFound v-if="!$multiIsGranted([PermissionAreas.Panel, PermissionAreas.External])" />
|
|
<div v-else>
|
|
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center">
|
|
<div>
|
|
<h2>
|
|
<Icon v="user-cog" />
|
|
<T>admin.header</T>
|
|
</h2>
|
|
|
|
<p v-if="stats" class="small">
|
|
<em>
|
|
Stats calculated at:
|
|
<span
|
|
:class="stats.calculatedAt < (newDate().getTime() - 24 * 60 * 60 * 1000) / 1000
|
|
? `badge bg-danger text-white`
|
|
: ''"
|
|
>
|
|
{{ $datetime(stats.calculatedAt) }}
|
|
</span>
|
|
</em>
|
|
</p>
|
|
</div>
|
|
<div class="form-check form-switch my-2">
|
|
<label>
|
|
<input v-model="filterAttention" class="form-check-input" type="checkbox" role="switch">
|
|
<Icon v="filter" />
|
|
Only show items requiring attention
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>General</h3>
|
|
|
|
<div class="row mb-4">
|
|
<AdminDashboardCard
|
|
v-if="stats && $multiIsGranted([PermissionAreas.Users, PermissionAreas.Community])"
|
|
v-show="!filterAttention"
|
|
icon="users"
|
|
header="Users"
|
|
link="/admin/users"
|
|
:counts="[
|
|
{ count: stats.overall.users },
|
|
{ name: 'admins', count: stats.overall.admins },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats && $multiIsGranted([PermissionAreas.Users, PermissionAreas.Community])"
|
|
v-show="!filterAttention || stats.overall.bansPending"
|
|
icon="ban"
|
|
header="Pending bans"
|
|
link="/admin/pending-bans"
|
|
:counts="[
|
|
{ count: stats.overall.bansPending, warning: 1, danger: 16 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats && $multiIsGranted([PermissionAreas.Users, PermissionAreas.Community])"
|
|
v-show="!filterAttention || stats.overall.userReports"
|
|
icon="siren-on"
|
|
header="Abuse reports"
|
|
link="/admin/abuse-reports"
|
|
:counts="[
|
|
{ count: stats.overall.userReports, warning: 1, danger: 16 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-show="!filterAttention && $isGranted(PermissionAreas.Panel)"
|
|
icon="user-cog"
|
|
link="/admin/moderation"
|
|
header="Moderation rules"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats && $isGranted(PermissionAreas.Code)"
|
|
v-show="!filterAttention || stats.overall.cardsQueue"
|
|
icon="id-card"
|
|
header="Cards queue"
|
|
:counts="[
|
|
{ count: stats.overall.cardsQueue, warning: 16, danger: 64 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats && $isGranted(PermissionAreas.Code)"
|
|
v-show="!filterAttention || stats.overall.linksQueue"
|
|
icon="link"
|
|
header="Links queue"
|
|
:counts="[
|
|
{ count: stats.overall.linksQueue, warning: 64, danger: 256 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-show="!filterAttention && $isGranted(PermissionAreas.Panel)"
|
|
icon="bell"
|
|
header="Email notifications"
|
|
>
|
|
<span v-for="(label, value) in { 0: 'Never', 1: 'Daily', 7: 'Weekly' }" class="form-check form-check-inline">
|
|
<input
|
|
:id="`notificationFrequency_${value}`"
|
|
v-model="adminNotifications"
|
|
class="form-check-input"
|
|
type="radio"
|
|
name="inlineRadioOptions"
|
|
:value="value"
|
|
>
|
|
<label class="form-check-label" :for="`notificationFrequency_${value}`">{{ label }}</label>
|
|
</span>
|
|
</AdminDashboardCard>
|
|
<AdminDashboardCard
|
|
v-show="!filterAttention"
|
|
v-if="$multiIsGranted([PermissionAreas.Users, PermissionAreas.Community])"
|
|
icon="user-secret"
|
|
header="@example"
|
|
>
|
|
<button type="button" class="btn btn-primary btn-sm" @click="impersonate('example@pronouns.page')">
|
|
Impersonate
|
|
</button>
|
|
</AdminDashboardCard>
|
|
<AdminDashboardCard
|
|
v-show="!filterAttention && $isGranted(PermissionAreas.Panel)"
|
|
icon="b:discord"
|
|
link="https://team-discord.pronouns.page"
|
|
header="Team Discord"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-show="!filterAttention && $isGranted(PermissionAreas.Panel)"
|
|
icon="palette"
|
|
link="/design"
|
|
header="Design guidelines"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-show="!filterAttention && $multiIsGranted([PermissionAreas.Panel, PermissionAreas.External])"
|
|
icon="file-spreadsheet"
|
|
link="/admin/timesheets"
|
|
header="Volunteering timesheets"
|
|
/>
|
|
</div>
|
|
|
|
<template v-for="{ name, extra, config, published, code: locale } in visibleLocales" :key="locale">
|
|
<h3 :style="`--font-headings: ${formatFonts(config.style.fontHeadings)}`">
|
|
{{ name }} {{ extra ? `(${extra})` : '' }}
|
|
<small v-if="!published" class="text-muted">(not published yet)</small>
|
|
</h3>
|
|
<div class="row mb-4">
|
|
<AdminDashboardCard
|
|
v-if="stats && $isGranted(PermissionAreas.Users, locale)"
|
|
v-show="!filterAttention"
|
|
:locale
|
|
icon="users"
|
|
header="Profiles"
|
|
link="/admin/profiles"
|
|
:counts="[
|
|
{ count: stats.locales[locale].users },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats &&
|
|
config.nouns &&
|
|
config.nouns.enabled &&
|
|
$isGranted(PermissionAreas.Nouns, locale) &&
|
|
(stats.locales[locale].nouns.approved > 0 || stats.locales[locale].nouns.awaiting > 0)"
|
|
v-show="!filterAttention || stats.locales[locale].nouns.awaiting"
|
|
:locale
|
|
icon="book"
|
|
header="Dictionary"
|
|
:link="`/${encodeURIComponent(config.nouns.route)}`"
|
|
:counts="[
|
|
{ count: stats.locales[locale].nouns.approved },
|
|
{ name: 'awaiting', count: stats.locales[locale].nouns.awaiting, warning: 1, danger: 16 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats &&
|
|
config.inclusive &&
|
|
config.inclusive.enabled &&
|
|
$isGranted(PermissionAreas.Inclusive, locale) &&
|
|
(
|
|
stats.locales[locale].inclusive.approved > 0 ||
|
|
stats.locales[locale].inclusive.awaiting > 0
|
|
)"
|
|
v-show="!filterAttention || stats.locales[locale].inclusive.awaiting"
|
|
:locale
|
|
icon="book-heart"
|
|
header="Inclusive"
|
|
:link="`/${encodeURIComponent(config.inclusive.route)}`"
|
|
:counts="[
|
|
{ count: stats.locales[locale].inclusive.approved },
|
|
{
|
|
name: 'awaiting',
|
|
count: stats.locales[locale].inclusive.awaiting,
|
|
warning: 1,
|
|
danger: 16,
|
|
},
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats &&
|
|
config.terminology &&
|
|
config.terminology.enabled &&
|
|
$isGranted(PermissionAreas.Terms, locale) &&
|
|
(stats.locales[locale].terms.approved > 0 || stats.locales[locale].terms.awaiting > 0)"
|
|
v-show="!filterAttention || stats.locales[locale].terms.awaiting"
|
|
:locale
|
|
icon="flag"
|
|
header="Terminology"
|
|
:link="`/${encodeURIComponent(config.terminology.route)}`"
|
|
:counts="[
|
|
{ count: stats.locales[locale].terms.approved },
|
|
{ name: 'awaiting', count: stats.locales[locale].terms.awaiting, warning: 1, danger: 16 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats &&
|
|
config.sources &&
|
|
config.sources.enabled &&
|
|
$isGranted(PermissionAreas.Sources, locale) &&
|
|
(stats.locales[locale].sources.approved > 0 || stats.locales[locale].sources.awaiting > 0)"
|
|
v-show="!filterAttention || stats.locales[locale].sources.awaiting"
|
|
:locale
|
|
icon="books"
|
|
header="Sources"
|
|
:link="`/${encodeURIComponent(config.sources.route)}`"
|
|
:counts="[
|
|
{ count: stats.locales[locale].sources.approved },
|
|
{ name: 'awaiting', count: stats.locales[locale].sources.awaiting, warning: 1, danger: 16 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="stats &&
|
|
config.names &&
|
|
config.names.enabled &&
|
|
$isGranted(PermissionAreas.Names, locale) &&
|
|
(stats.locales[locale].names.approved > 0 || stats.locales[locale].names.awaiting > 0)"
|
|
v-show="!filterAttention || stats.locales[locale].names.awaiting"
|
|
:locale
|
|
icon="signature"
|
|
header="Names"
|
|
:link="`/${encodeURIComponent(config.names.route)}`"
|
|
:counts="[
|
|
{ count: stats.locales[locale].names.approved },
|
|
{ name: 'awaiting', count: stats.locales[locale].names.awaiting, warning: 1, danger: 16 },
|
|
]"
|
|
/>
|
|
<AdminDashboardCard
|
|
v-if="config.links.enabled &&
|
|
config.links.blog &&
|
|
$isGranted(PermissionAreas.Blog, locale)"
|
|
v-show="!filterAttention"
|
|
:locale
|
|
icon="pen-nib"
|
|
header="Blog"
|
|
>
|
|
<LocaleLink link="/admin/blog/editor" class="btn btn-sm btn-secondary text-white" :locale>
|
|
Editor
|
|
</LocaleLink>
|
|
</AdminDashboardCard>
|
|
<AdminDashboardCard
|
|
v-if="stats &&
|
|
$isGranted(PermissionAreas.Translations, locale) &&
|
|
(
|
|
stats.locales[locale].translations.missing > 0 ||
|
|
stats.locales[locale].translations.awaitingApproval > 0
|
|
) ||
|
|
stats &&
|
|
$isGranted(PermissionAreas.Code, locale) &&
|
|
stats.locales[locale].translations.awaitingMerge > 0"
|
|
v-show="!filterAttention ||
|
|
stats.locales[locale].translations.missing ||
|
|
stats.locales[locale].translations.awaitingApproval ||
|
|
stats?.locales[locale].translations.awaitingMerge"
|
|
:locale
|
|
icon="language"
|
|
header="Translations"
|
|
:counts="[
|
|
{
|
|
name: 'missing',
|
|
link: '/admin/translations/missing',
|
|
count: stats.locales[locale].translations.missing,
|
|
warning: 1,
|
|
danger: 16,
|
|
enabled: $isGranted(PermissionAreas.Translations, locale),
|
|
},
|
|
{
|
|
name: 'proposed',
|
|
link: '/admin/translations/awaiting',
|
|
count: stats.locales[locale].translations.awaitingApproval,
|
|
warning: 1,
|
|
danger: 16,
|
|
enabled: $isGranted(PermissionAreas.Translations, locale),
|
|
},
|
|
{
|
|
name: 'not merged',
|
|
link: '/admin/translations/awaiting',
|
|
count: stats.locales[locale].translations.awaitingMerge,
|
|
warning: 1,
|
|
danger: 16,
|
|
enabled: $isGranted(PermissionAreas.Code, locale),
|
|
},
|
|
]"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</Page>
|
|
</template>
|