This commit is contained in:
@@ -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
1694
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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 };
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">​</div>
|
<div class="stat-desc">​</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">​</div>
|
<div class="stat-desc">​</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>
|
||||||
|
|||||||
@@ -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
12
src/styles/global.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -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/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user