add live statistics
All checks were successful
deploy / build-and-deploy (push) Successful in 22s

This commit is contained in:
2025-11-11 01:53:56 +01:00
parent dfc1425c6b
commit 5ce2db9040
26 changed files with 1168 additions and 817 deletions

View File

@@ -1,7 +1,6 @@
// @ts-check // @ts-check
import { defineConfig, envField } from 'astro/config'; import { defineConfig, envField } from 'astro/config';
import icon from 'astro-icon';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import svelte, { vitePreprocess } from '@astrojs/svelte'; import svelte, { vitePreprocess } from '@astrojs/svelte';
@@ -26,7 +25,7 @@ export default defineConfig({
plugins: [tailwindcss()] plugins: [tailwindcss()]
}, },
integrations: [icon(), svelte({ preprocess: vitePreprocess() })], integrations: [svelte({ preprocess: vitePreprocess() })],
env: { env: {
schema: { schema: {
@@ -43,6 +42,9 @@ export default defineConfig({
WEBHOOK_ENDPOINT: envField.string({ context: 'server', access: 'secret', optional: true }), WEBHOOK_ENDPOINT: envField.string({ context: 'server', access: 'secret', optional: true }),
STATISTICS_ENDPOINT: envField.string({ context: 'server', access: 'secret', optional: true }),
STATISTICS_INTERVAL: envField.number({ context: 'server', access: 'secret', default: 60 * 30 }),
TEAMSPEAK_LINK: envField.string({ context: 'server', access: 'secret', default: 'http://example.com' }), TEAMSPEAK_LINK: envField.string({ context: 'server', access: 'secret', default: 'http://example.com' }),
DISCORD_LINK: envField.string({ context: 'server', access: 'secret', default: 'http://example.com' }), DISCORD_LINK: envField.string({ context: 'server', access: 'secret', default: 'http://example.com' }),
PAYPAL_LINK: envField.string({ context: 'server', access: 'secret', default: 'http://example.com' }), PAYPAL_LINK: envField.string({ context: 'server', access: 'secret', default: 'http://example.com' }),

1694
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,11 @@
"dependencies": { "dependencies": {
"@astrojs/node": "^9.5.0", "@astrojs/node": "^9.5.0",
"@astrojs/svelte": "^7.2.2", "@astrojs/svelte": "^7.2.2",
"@tailwindcss/vite": "^4.1.17",
"@iconify-json/fa-brands": "^1.2.2", "@iconify-json/fa-brands": "^1.2.2",
"@iconify-json/heroicons": "^1.2.3", "@iconify-json/heroicons": "^1.2.3",
"@iconify/svelte": "^5.1.0", "@iconify/tailwind4": "^1.1.0",
"@tailwindcss/vite": "^4.1.17",
"astro": "^5.15.4", "astro": "^5.15.4",
"astro-icon": "=1.1.2",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"daisyui": "^5.4.7", "daisyui": "^5.4.7",
"drizzle-orm": "^0.44.7", "drizzle-orm": "^0.44.7",

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addAdmin, fetchAdmins } from '@app/admin/admins/admins.ts'; import { addAdmin, fetchAdmins } from '@app/admin/admins/admins.ts';
import { Permissions } from '@util/permissions.ts'; import { Permissions } from '@util/permissions.ts';
@@ -15,7 +14,7 @@
<div> <div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}> <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> <span>Neuer Admin</span>
</button> </button>
</div> </div>

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { fetchBlockedUsers, addBlockedUser } from '@app/admin/blockedUsers/blockedUsers.ts'; import { fetchBlockedUsers, addBlockedUser } from '@app/admin/blockedUsers/blockedUsers.ts';
@@ -14,7 +13,7 @@
<div> <div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}> <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> <span>Neuer blockierter Nutzer</span>
</button> </button>
</div> </div>

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addDirectInvitation, fetchDirectInvitations } from '@app/admin/directInvitations/directInvitations.ts'; import { addDirectInvitation, fetchDirectInvitations } from '@app/admin/directInvitations/directInvitations.ts';
@@ -14,7 +13,7 @@
<div> <div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}> <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> <span>Neue direkte Einladung</span>
</button> </button>
</div> </div>

View File

@@ -5,7 +5,6 @@
import Select from '@components/input/Select.svelte'; import Select from '@components/input/Select.svelte';
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts'; import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts'; import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import Icon from '@iconify/svelte';
import UserSearch from '@components/admin/search/UserSearch.svelte'; import UserSearch from '@components/admin/search/UserSearch.svelte';
// html bindings // html bindings
@@ -90,7 +89,9 @@
> >
<div class="absolute right-2 top-2"> <div class="absolute right-2 top-2">
<div class="dropdown dropdown-end"> <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 --> <!-- 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"> <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> <li><button onclick={() => onCopyPublicLink(report?.urlHash)}>Öffentlichen Report Link kopieren</button></li>

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte';
import Input from '@components/input/Input.svelte'; import Input from '@components/input/Input.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addReport, fetchReports } from '@app/admin/reports/reports.ts'; import { addReport, fetchReports } from '@app/admin/reports/reports.ts';
@@ -27,7 +26,7 @@
</fieldset> </fieldset>
<div class="divider my-1"></div> <div class="divider my-1"></div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}> <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> <span>Neuer Report</span>
</button> </button>
</div> </div>

View File

@@ -1,5 +1,4 @@
<script> <script>
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addStrikeReason, fetchStrikeReasons } from '@app/admin/strikeReasons/strikeReasons.js'; import { addStrikeReason, fetchStrikeReasons } from '@app/admin/strikeReasons/strikeReasons.js';
@@ -14,7 +13,7 @@
<div> <div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}> <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> <span>Neuer Strikegrund</span>
</button> </button>
</div> </div>

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '@iconify/svelte';
import Input from '@components/input/Input.svelte'; import Input from '@components/input/Input.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte'; import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addUser, fetchUsers } from '@app/admin/users/users.ts'; import { addUser, fetchUsers } from '@app/admin/users/users.ts';
@@ -22,7 +21,7 @@
</fieldset> </fieldset>
<div class="divider my-1"></div> <div class="divider my-1"></div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}> <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> <span>Neuer Nutzer</span>
</button> </button>
</div> </div>

View 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">&#8203;</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">&#8203;</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>

View 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 };
}

View File

@@ -2,7 +2,6 @@
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import SortableTr from '@components/admin/table/SortableTr.svelte'; import SortableTr from '@components/admin/table/SortableTr.svelte';
import SortableTh from '@components/admin/table/SortableTh.svelte'; import SortableTh from '@components/admin/table/SortableTh.svelte';
import Icon from '@iconify/svelte';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { getObjectEntryByKey } from '@util/objects.ts'; import { getObjectEntryByKey } from '@util/objects.ts';
@@ -64,12 +63,12 @@
<td> <td>
{#if onEdit} {#if onEdit}
<button class="cursor-pointer" onclick={() => onEdit(d)}> <button class="cursor-pointer" onclick={() => onEdit(d)}>
<Icon icon="heroicons:pencil-square" /> <span class="iconify iconify-[heroicons--pencil-square]"></span>
</button> </button>
{/if} {/if}
{#if onDelete} {#if onDelete}
<button class="cursor-pointer" onclick={() => onDelete(d)}> <button class="cursor-pointer" onclick={() => onDelete(d)}>
<Icon icon="heroicons:trash" /> <span class="iconify iconify-[heroicons--trash]"></span>
</button> </button>
{/if} {/if}
</td> </td>

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getContext, type Snippet } from 'svelte'; import { getContext, type Snippet } from 'svelte';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import Icon from '@iconify/svelte';
// types // types
interface Props { interface Props {
@@ -37,9 +36,9 @@
<button class="flex items-center gap-1" onclick={() => onButtonClick()}> <button class="flex items-center gap-1" onclick={() => onButtonClick()}>
<span>{@render children?.()}</span> <span>{@render children?.()}</span>
{#if $headerKey === key && asc} {#if $headerKey === key && asc}
<Icon icon="heroicons:chevron-up-16-solid" /> <span class="iconify iconify-[heroicons--chevron-up-16-solid]"></span>
{:else} {:else}
<Icon icon="heroicons:chevron-down-16-solid" /> <span class="iconify iconify-[heroicons--chevron-down-16-solid]"></span>
{/if} {/if}
</button> </button>
{:else} {:else}

View File

@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import Icon from '@iconify/svelte';
interface Props { interface Props {
id?: string; id?: string;
@@ -44,19 +43,21 @@
/> />
<button <button
type="button" type="button"
aria-label="Hide password"
class="absolute right-2 cursor-pointer z-10" class="absolute right-2 cursor-pointer z-10"
class:hidden={!visible} class:hidden={!visible}
onclick={() => (visible = !visible)} onclick={() => (visible = !visible)}
> >
<Icon icon="heroicons:eye-16-solid" width={22} /> <span class="iconify iconify-[heroicons--eye-16-solid] w-[22px]"></span>
</button> </button>
<button <button
type="button" type="button"
aria-label="Show password"
class="absolute right-2 cursor-pointer z-10" class="absolute right-2 cursor-pointer z-10"
class:hidden={visible} class:hidden={visible}
onclick={() => (visible = !visible)} onclick={() => (visible = !visible)}
> >
<Icon icon="heroicons:eye-slash-16-solid" width={22} /> <span class="iconify iconify-[heroicons--eye-slash-16-solid] w-[22px]"></span>
</button> </button>
</div> </div>
<p class="fieldset-label"> <p class="fieldset-label">

View File

@@ -1,4 +1,5 @@
--- ---
import '@styles/global.css';
import { ClientRouter } from 'astro:transitions'; import { ClientRouter } from 'astro:transitions';
import { BASE_PATH } from 'astro:env/server'; import { BASE_PATH } from 'astro:env/server';
import logo512 from '@assets/img/logo-512.webp'; import logo512 from '@assets/img/logo-512.webp';

View File

@@ -1,8 +1,7 @@
--- ---
import '@assets/admin_layout.css'; import '@styles/adminLayout.css';
import BaseLayout from '../BaseLayout.astro'; import BaseLayout from '../BaseLayout.astro';
import { ClientRouter } from 'astro:transitions'; import { ClientRouter } from 'astro:transitions';
import { Icon } from 'astro-icon/components';
import Popup from '@components/popup/Popup.svelte'; import Popup from '@components/popup/Popup.svelte';
import ConfirmPopup from '@components/popup/ConfirmPopup.svelte'; import ConfirmPopup from '@components/popup/ConfirmPopup.svelte';
import { Session } from '@util/session.ts'; import { Session } from '@util/session.ts';
@@ -19,24 +18,24 @@ const preTabs = [
{ {
href: '', href: '',
name: 'Startseite', name: 'Startseite',
icon: 'heroicons:computer-desktop-20-solid' iconClass: 'iconify-[heroicons--computer-desktop-20-solid]'
} }
]; ];
const adminTabs = [ const adminTabs = [
{ {
href: 'admin/users', href: 'admin/users',
name: 'Registrierte Nutzer', name: 'Registrierte Nutzer',
icon: 'heroicons:user', iconClass: 'iconify-[heroicons--user]',
subTabs: [ subTabs: [
{ {
href: 'admin/users/direct_invitations', href: 'admin/users/direct_invitations',
name: 'Direkte Einladungen', name: 'Direkte Einladungen',
icon: 'heroicons:envelope' iconClass: 'iconify-[heroicons--envelope]'
}, },
{ {
href: 'admin/users/blocked', href: 'admin/users/blocked',
name: 'Blockierte Nutzer', name: 'Blockierte Nutzer',
icon: 'heroicons:user-minus' iconClass: 'iconify-[heroicons--user-minus]'
} }
], ],
enabled: session?.permissions.users enabled: session?.permissions.users
@@ -44,24 +43,24 @@ const adminTabs = [
{ {
href: 'admin/reports', href: 'admin/reports',
name: 'Reports', name: 'Reports',
icon: 'heroicons:flag', iconClass: 'iconify-[heroicons--flag]',
enabled: session?.permissions.reports enabled: session?.permissions.reports
}, },
{ {
href: 'admin/feedback', href: 'admin/feedback',
name: 'Feedback', name: 'Feedback',
icon: 'heroicons:book-open', iconClass: 'iconify-[heroicons--book-open]',
enabled: session?.permissions.feedback enabled: session?.permissions.feedback
}, },
{ {
href: 'admin/admins', href: 'admin/admins',
name: 'Website Admins', name: 'Website Admins',
icon: 'heroicons:code-bracket-16-solid', iconClass: 'iconify-[heroicons--code-bracket-16-solid]',
subTabs: [ subTabs: [
{ {
href: 'admin/admins/strike_reasons', href: 'admin/admins/strike_reasons',
name: 'Strikegründe', name: 'Strikegründe',
icon: 'heroicons:shield-exclamation' iconClass: 'iconify-[heroicons--shield-exclamation]'
} }
], ],
enabled: session?.permissions.admin enabled: session?.permissions.admin
@@ -69,13 +68,13 @@ const adminTabs = [
{ {
href: 'admin/settings', href: 'admin/settings',
name: 'Einstellungen', name: 'Einstellungen',
icon: 'heroicons:adjustments-horizontal', iconClass: 'iconify-[heroicons--adjustments-horizontal]',
enabled: session?.permissions.settings enabled: session?.permissions.settings
}, },
{ {
href: 'admin/tools', href: 'admin/tools',
name: 'Tools', name: 'Tools',
icon: 'heroicons:wrench-screwdriver', iconClass: 'iconify-[heroicons--wrench-screwdriver]',
enabled: session?.permissions.tools enabled: session?.permissions.tools
} }
]; ];
@@ -89,7 +88,7 @@ const adminTabs = [
preTabs.map((tab) => ( preTabs.map((tab) => (
<li> <li>
<a href={tab.href}> <a href={tab.href}>
<Icon name={tab.icon} /> <span class="iconify" class:list={tab.iconClass} />
<span>{tab.name}</span> <span>{tab.name}</span>
</a> </a>
</li> </li>
@@ -102,7 +101,7 @@ const adminTabs = [
tab.enabled && ( tab.enabled && (
<li> <li>
<a href={tab.href}> <a href={tab.href}>
<Icon name={tab.icon} /> <span class="iconify" class:list={tab.iconClass} />
<span>{tab.name}</span> <span>{tab.name}</span>
</a> </a>
{tab.subTabs && ( {tab.subTabs && (
@@ -110,7 +109,7 @@ const adminTabs = [
{tab.subTabs.map((subTab) => ( {tab.subTabs.map((subTab) => (
<li> <li>
<a href={subTab.href}> <a href={subTab.href}>
<Icon name={subTab.icon} /> <span class="iconify" class:list={subTab.iconClass} />
<span>{subTab.name}</span> <span>{subTab.name}</span>
</a> </a>
</li> </li>
@@ -130,7 +129,7 @@ const adminTabs = [
} }
<li class:list={[Astro.slots.has('actions') ? null : 'mt-auto']}> <li class:list={[Astro.slots.has('actions') ? null : 'mt-auto']}>
<button id="logout"> <button id="logout">
<Icon name="heroicons:arrow-left-end-on-rectangle" /> <span class="iconify iconify-[heroicons--arrow-left-end-on-rectangle]"></span>
<span>Ausloggen</span> <span>Ausloggen</span>
</button> </button>
</li> </li>

View File

@@ -1,5 +1,5 @@
--- ---
import '@assets/admin_layout.css'; import '@styles/adminLayout.css';
import BaseLayout from '../BaseLayout.astro'; import BaseLayout from '../BaseLayout.astro';
import { ClientRouter } from 'astro:transitions'; import { ClientRouter } from 'astro:transitions';

View File

@@ -1,7 +1,6 @@
--- ---
import '@assets/website_layout.css'; import '@styles/websiteLayout.css';
import BaseLayout from '../BaseLayout.astro'; import BaseLayout from '../BaseLayout.astro';
import { Icon } from 'astro-icon/components';
import Menu from '@app/layout/Menu.svelte'; import Menu from '@app/layout/Menu.svelte';
interface Props { interface Props {
@@ -32,10 +31,10 @@ const { title, description, footer = true } = Astro.props;
</div> </div>
<div class="hidden sm:flex gap-2 items-center"> <div class="hidden sm:flex gap-2 items-center">
<a href="ts3server://mhsl.eu?port=9987" title="TeamSpeak" target="_blank"> <a href="ts3server://mhsl.eu?port=9987" title="TeamSpeak" target="_blank">
<Icon name="fa-brands:teamspeak" /> <span class="iconify iconify-[fa-brands--teamspeak]"></span>
</a> </a>
<a href="https://discord.gg/EBGefWPc2K" title="Discord" target="_blank"> <a href="https://discord.gg/EBGefWPc2K" title="Discord" target="_blank">
<Icon name="fa-brands:discord" /> <span class="iconify iconify-[fa-brands--discord]"></span>
</a> </a>
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@ import Popup from '@components/popup/Popup.svelte';
<form id="login" class="flex flex-col items-center"> <form id="login" class="flex flex-col items-center">
<div> <div>
<Input id="username" type="text" label="Nutzername" required /> <Input id="username" type="text" label="Nutzername" required />
<Password id="password" label="Passwort" required /> <Password id="password" label="Passwort" required client:load />
</div> </div>
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-neutral">Login</button> <button class="btn btn-neutral">Login</button>

View File

@@ -1,13 +1,12 @@
--- ---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro'; import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import { Icon } from 'astro-icon/components';
const team = [ const team = [
{ {
name: 'Elias', name: 'Elias',
nickname: 'MineTec', nickname: 'MineTec',
roles: ['Gründer', 'Support', 'Organisation', 'Softwareentwicklung', 'Systemadministrator'], roles: ['Gründer', 'Support', 'Organisation', 'Softwareentwicklung', 'Systemadministrator'],
links: [{ name: 'Website', href: 'https://mhsl.eu/aboutme/', icon: 'heroicons:globe-alt-solid' }] links: [{ name: 'Website', href: 'https://mhsl.eu/aboutme/', iconClass: 'iconify-[heroicons--globe-alt-solid]' }]
}, },
{ {
name: 'Jannik', name: 'Jannik',
@@ -23,13 +22,19 @@ const team = [
name: 'Ruben', name: 'Ruben',
nickname: 'bytedream', nickname: 'bytedream',
roles: ['Softwareentwicklung'], roles: ['Softwareentwicklung'],
links: [{ name: 'Website', href: 'https://bytedream.dev', icon: 'heroicons:globe-alt-solid' }] links: [{ name: 'Website', href: 'https://bytedream.dev', iconClass: 'iconify-[heroicons--globe-alt-solid]' }]
}, },
{ {
name: 'Lars', name: 'Lars',
nickname: '28Pupsi28', nickname: '28Pupsi28',
roles: ['Support', 'Softwareentwicklung'], roles: ['Support', 'Softwareentwicklung'],
links: [{ name: 'Website', href: 'https://mathemann.ddns.net/turtle_game/', icon: 'heroicons:globe-alt-solid' }] links: [
{
name: 'Website',
href: 'https://mathemann.ddns.net/turtle_game/',
iconClass: 'iconify-[heroicons--globe-alt-solid]'
}
]
}, },
{ {
name: 'Hanad', name: 'Hanad',
@@ -73,7 +78,7 @@ const team = [
target="_blank" target="_blank"
title={link.name} title={link.name}
> >
<Icon name={link.icon} size={22} /> <span class="iconify size-[22px]" class:list={[link.iconClass]} />
</a> </a>
))} ))}
</div> </div>

View File

@@ -1,12 +1,12 @@
--- ---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro'; import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import Countdown from '@app/website/index/Countdown.svelte'; import Countdown from '@app/website/index/Countdown.svelte';
import LiveStats from '@app/website/index/LiveStats.svelte';
import Craftattack from '@assets/img/craftattack.webp'; import Craftattack from '@assets/img/craftattack.webp';
import Background from '@assets/img/background.webp'; import Background from '@assets/img/background.webp';
import { START_DATE } from 'astro:env/server'; import { START_DATE, STATISTICS_ENDPOINT } from 'astro:env/server';
import { getSetting, SettingKey } from '@util/settings'; import { getSetting, SettingKey } from '@util/settings';
import { db } from '@db/database.ts'; import { db } from '@db/database.ts';
import { Icon } from 'astro-icon/components';
const signupEnabled = await getSetting(db, SettingKey.SignupEnabled, false); const signupEnabled = await getSetting(db, SettingKey.SignupEnabled, false);
const signupInfoMessage = await getSetting(db, SettingKey.SignupInfoMessage); const signupInfoMessage = await getSetting(db, SettingKey.SignupInfoMessage);
@@ -96,14 +96,22 @@ const information = [
</div> </div>
</div> </div>
<div class="flex flex-col xl:flex-row justify-center items-center py-20 bg-base-100"> <div class="flex flex-col justify-center items-center py-20 bg-base-100">
{
STATISTICS_ENDPOINT && (
<>
<LiveStats />
<div class="divider divider-horizontal mx-auto my-6 h-18" />
</>
)
}
<div> <div>
<h3 class="text-center text-2xl mb-6">2024/2025 in Zahlen</h3> <h3 class="text-center text-2xl mb-6">2024/2025 in Zahlen</h3>
<div class="flex flex-col lg:flex-row gap-4"> <div class="flex flex-col lg:flex-row gap-4">
<div class="stats stats-vertical xl:stats-horizontal shadow"> <div class="bg-base-200 stats stats-vertical xl:stats-horizontal shadow">
<div class="stat"> <div class="stat">
<div class="stat-figure"> <div class="stat-figure">
<Icon name="heroicons:wrench-screwdriver-solid" size="1.5em" /> <span class="iconify iconify-[heroicons--wrench-screwdriver-solid] size-[1.5em]"></span>
</div> </div>
<div class="stat-title">Abgebaute Blöcke</div> <div class="stat-title">Abgebaute Blöcke</div>
<div class="stat-value">17M</div> <div class="stat-value">17M</div>
@@ -111,27 +119,27 @@ const information = [
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-figure"> <div class="stat-figure">
<Icon name="heroicons:users-solid" size="1.5em" /> <span class="iconify iconify-[heroicons--users-solid] size-[1.5em]"></span>
</div> </div>
<div class="stat-title">Teilnehmer</div> <div class="stat-title">Teilnehmer</div>
<div class="stat-value">161</div> <div class="stat-value">161</div>
<div class="stat-desc">&#8203;</div> <div class="stat-desc">&#8203;</div>
</div> </div>
</div> </div>
<div class="stats stats-vertical xl:stats-horizontal shadow h-min xl:h-[initial]"> <div class="bg-base-200 stats stats-vertical xl:stats-horizontal shadow h-min xl:h-[initial]">
<div class="stat"> <div class="stat">
<div class="stat-figure"> <div class="stat-figure">
<Icon name="heroicons:clock-solid" size="1.5em" /> <span class="iconify iconify-[heroicons--clock-solid] size-[1.5em]"></span>
</div> </div>
<div class="stat-title">Gesamtspielzeit</div> <div class="stat-title">Gesamtspielzeit</div>
<div class="stat-value">276 Tage</div> <div class="stat-value">276 Tage</div>
<div class="stat-desc">&#8203;</div> <div class="stat-desc">&#8203;</div>
</div> </div>
</div> </div>
<div class="stats stats-vertical xl:stats-horizontal shadow"> <div class="bg-base-200 stats stats-vertical xl:stats-horizontal shadow">
<div class="stat"> <div class="stat">
<div class="stat-figure"> <div class="stat-figure">
<Icon name="crosshairs" size="1.5em" /> <span class="iconify iconify-[local--crosshairs] size-[1.5em]"></span>
</div> </div>
<div class="stat-title">Getötete Monster</div> <div class="stat-title">Getötete Monster</div>
<div class="stat-value">751K</div> <div class="stat-value">751K</div>
@@ -139,7 +147,7 @@ const information = [
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-figure"> <div class="stat-figure">
<Icon name="skull" size="1.5em" /> <span class="iconify iconify-[local--skull] size-[1.5em]"></span>
</div> </div>
<div class="stat-title">Spieler Tode</div> <div class="stat-title">Spieler Tode</div>
<div class="stat-value">2468</div> <div class="stat-value">2468</div>

View File

@@ -8,17 +8,17 @@
@font-face { @font-face {
font-family: Geist; font-family: Geist;
src: url('fonts/Geist.ttf') format('truetype'); src: url('../assets/fonts/Geist.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: GeistMono; font-family: GeistMono;
src: url('fonts/GeistMono.ttf') format('truetype'); src: url('../assets/fonts/GeistMono.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: Minecraft; font-family: Minecraft;
src: url('./fonts/MinecraftRegular.otf') format('opentype'); src: url('../assets/fonts/MinecraftRegular.otf') format('opentype');
} }
@theme { @theme {

12
src/styles/global.css Normal file
View File

@@ -0,0 +1,12 @@
@import 'tailwindcss';
@plugin "@iconify/tailwind4" {
prefix: iconify;
icon-sets: from-folder(local, './src/icons');
}
@layer utilities {
.iconify {
vertical-align: middle;
}
}

View File

@@ -8,17 +8,17 @@
@font-face { @font-face {
font-family: Geist; font-family: Geist;
src: url('fonts/Geist.ttf') format('truetype'); src: url('../assets/fonts/Geist.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: GeistMono; font-family: GeistMono;
src: url('fonts/GeistMono.ttf') format('truetype'); src: url('../assets/fonts/GeistMono.ttf') format('truetype');
} }
@font-face { @font-face {
font-family: Minecraft; font-family: Minecraft;
src: url('./fonts/MinecraftRegular.otf') format('opentype'); src: url('../assets/fonts/MinecraftRegular.otf') format('opentype');
} }
@theme { @theme {

View File

@@ -10,6 +10,7 @@
"@components/*": ["./src/components/*"], "@components/*": ["./src/components/*"],
"@db/*": ["./src/db/*"], "@db/*": ["./src/db/*"],
"@layouts/*": ["./src/layouts/*"], "@layouts/*": ["./src/layouts/*"],
"@styles/*": ["./src/styles/*"],
"@util/*": ["./src/util/*"] "@util/*": ["./src/util/*"]
} }
} }