This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||
import { addAdmin, fetchAdmins } from '@app/admin/admins/admins.ts';
|
||||
import { Permissions } from '@util/permissions.ts';
|
||||
@@ -15,7 +14,7 @@
|
||||
|
||||
<div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
|
||||
<span>Neuer Admin</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||
import { fetchBlockedUsers, addBlockedUser } from '@app/admin/blockedUsers/blockedUsers.ts';
|
||||
|
||||
@@ -14,7 +13,7 @@
|
||||
|
||||
<div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
|
||||
<span>Neuer blockierter Nutzer</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||
import { addDirectInvitation, fetchDirectInvitations } from '@app/admin/directInvitations/directInvitations.ts';
|
||||
|
||||
@@ -14,7 +13,7 @@
|
||||
|
||||
<div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
|
||||
<span>Neue direkte Einladung</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import Select from '@components/input/Select.svelte';
|
||||
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
|
||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||
import Icon from '@iconify/svelte';
|
||||
import UserSearch from '@components/admin/search/UserSearch.svelte';
|
||||
|
||||
// html bindings
|
||||
@@ -90,7 +89,9 @@
|
||||
>
|
||||
<div class="absolute right-2 top-2">
|
||||
<div class="dropdown dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost"><Icon icon="heroicons:share" /></div>
|
||||
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost">
|
||||
<span class="iconify iconify-[heroicons--share]"></span>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
<ul tabindex="0" class="menu dropdown-content bg-base-100 rounded-box z-1 p-2 shadow-sm w-max">
|
||||
<li><button onclick={() => onCopyPublicLink(report?.urlHash)}>Öffentlichen Report Link kopieren</button></li>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import Input from '@components/input/Input.svelte';
|
||||
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||
import { addReport, fetchReports } from '@app/admin/reports/reports.ts';
|
||||
@@ -27,7 +26,7 @@
|
||||
</fieldset>
|
||||
<div class="divider my-1"></div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
|
||||
<span>Neuer Report</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
import Icon from '@iconify/svelte';
|
||||
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||
import { addStrikeReason, fetchStrikeReasons } from '@app/admin/strikeReasons/strikeReasons.js';
|
||||
|
||||
@@ -14,7 +13,7 @@
|
||||
|
||||
<div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
|
||||
<span>Neuer Strikegrund</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import Input from '@components/input/Input.svelte';
|
||||
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||
import { addUser, fetchUsers } from '@app/admin/users/users.ts';
|
||||
@@ -22,7 +21,7 @@
|
||||
</fieldset>
|
||||
<div class="divider my-1"></div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span class="iconify iconify-[heroicons--plus-16-solid]"></span>
|
||||
<span>Neuer Nutzer</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
60
src/app/website/index/LiveStats.svelte
Normal file
60
src/app/website/index/LiveStats.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { STATISTICS_INTERVAL } from 'astro:env/server';
|
||||
import { statistics } from '@app/website/index/liveStats.ts';
|
||||
|
||||
function transformPlaytimeMinutes(playtimeMinutes: number) {
|
||||
return playtimeMinutes < 60 * 24
|
||||
? `${Math.floor(playtimeMinutes / 60)} Stunden`
|
||||
: `${Math.floor(playtimeMinutes / 60 / 24)} Tage`;
|
||||
}
|
||||
|
||||
function transformMobKills(mobKills: number) {
|
||||
return mobKills < 1000 ? `${mobKills}` : `${Math.floor(mobKills / 1000)}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex flex-row items-start mb-6">
|
||||
<div class="tooltip" data-tip="* Die Statistiken werden alle {STATISTICS_INTERVAL / 60} Minuten aktualisiert">
|
||||
<h3 class="text-2xl">Live<span class="font-geist text-[18px]">*</span> Statistiken</h3>
|
||||
</div>
|
||||
<div class="inline-grid *:[grid-area:1/1] mt-1 ml-0.5">
|
||||
<div class="status status-info animate-ping"></div>
|
||||
<div class="status status-info"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="bg-base-200 stats stats-vertical xl:stats-horizontal shadow h-min xl:h-[initial]">
|
||||
<div class="stat">
|
||||
<div class="stat-figure">
|
||||
<span class="iconify iconify-[heroicons--clock-solid] size-[1.5em]"></span>
|
||||
</div>
|
||||
<div class="stat-title">Gesamtspielzeit</div>
|
||||
<div class="stat-value">
|
||||
{statistics != null ? transformPlaytimeMinutes(statistics.playtimeMinutes) : 'n/a'}
|
||||
</div>
|
||||
<div class="stat-desc">​</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-base-200 stats stats-vertical xl:stats-horizontal shadow">
|
||||
<div class="stat">
|
||||
<div class="stat-figure">
|
||||
<span class="iconify iconify-[local--crosshairs] size-[1.5em]"></span>
|
||||
</div>
|
||||
<div class="stat-title">Getötete Monster</div>
|
||||
<div class="stat-value">{statistics != null ? transformMobKills(statistics.mobKills) : 'n/a'}</div>
|
||||
<div class="stat-desc">​</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-figure">
|
||||
<span class="iconify iconify-[local--skull] size-[1.5em]"></span>
|
||||
</div>
|
||||
<div class="stat-title">Spieler Tode</div>
|
||||
<div class="stat-value">{statistics?.playerDeaths ?? 'n/a'}</div>
|
||||
<div class="stat-desc">
|
||||
<span class="underline">{statistics?.playerKills ?? 'n/a'}</span> davon durch andere Spieler
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
67
src/app/website/index/liveStats.ts
Normal file
67
src/app/website/index/liveStats.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { STATISTICS_ENDPOINT, STATISTICS_INTERVAL } from 'astro:env/server';
|
||||
import { logger } from '@util/log.ts';
|
||||
|
||||
export let statistics: Awaited<ReturnType<typeof fetchStatistics>> | null = null;
|
||||
if (STATISTICS_ENDPOINT) {
|
||||
statistics = await fetchStatistics().catch((_) => null);
|
||||
setInterval(
|
||||
() =>
|
||||
fetchStatistics()
|
||||
.catch((_) => null)
|
||||
.then((val) => (statistics = val)),
|
||||
STATISTICS_INTERVAL * 60 * 1000
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchStatistics() {
|
||||
const response = (await fetch(STATISTICS_ENDPOINT!, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
categories: [{ name: 'PLAY_ONE_MINUTE' }, { name: 'PLAYER_KILLS' }, { name: 'DEATHS' }, { name: 'MOB_KILLS' }]
|
||||
})
|
||||
}).catch((e) => {
|
||||
logger.warn(
|
||||
{
|
||||
error: e
|
||||
},
|
||||
'could not fetch statistics'
|
||||
);
|
||||
throw e;
|
||||
})) as Response;
|
||||
|
||||
type StatisticName = 'PLAY_ONE_MINUTE' | 'PLAYER_KILLS' | 'DEATHS' | 'MOB_KILLS';
|
||||
type ResponseData = {
|
||||
response: {
|
||||
playerStatistics: {
|
||||
playerName: string;
|
||||
statistics: { name: StatisticName; value: number }[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
const responseData: ResponseData = await response.json();
|
||||
|
||||
let playtimeMinutes = 0;
|
||||
let playerKills = 0;
|
||||
let playerDeaths = 0;
|
||||
let mobKills = 0;
|
||||
for (const player of responseData.response.playerStatistics) {
|
||||
for (const statistic of player.statistics) {
|
||||
switch (statistic.name) {
|
||||
case 'PLAY_ONE_MINUTE':
|
||||
playtimeMinutes += statistic.value / 20 / 60;
|
||||
break;
|
||||
case 'PLAYER_KILLS':
|
||||
playerKills += statistic.value;
|
||||
break;
|
||||
case 'DEATHS':
|
||||
playerDeaths += statistic.value;
|
||||
break;
|
||||
case 'MOB_KILLS':
|
||||
mobKills += statistic.value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { playtimeMinutes, playerKills, playerDeaths, mobKills };
|
||||
}
|
||||
Reference in New Issue
Block a user