rewrite website

This commit is contained in:
2025-10-13 17:22:49 +02:00
parent a6d910f56a
commit 32f28e5324
263 changed files with 17904 additions and 14451 deletions

14
src/pages/404.astro Normal file
View File

@@ -0,0 +1,14 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
---
<WebsiteLayout title="Seite nicht gefunden">
<div class="flex flex-col justify-center items-center gap-4 h-full w-full absolute">
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">404: Seite nicht gefunden</h1>
<p class="flex gap-2 text-center">
Es wurde keine Seite unter der URL <code class="contents mx-1 md:block md:my-0 md:whitespace-pre"
>{Astro.url.pathname}</code
> gefunden.
</p>
</div>
</WebsiteLayout>

12
src/pages/500.astro Normal file
View File

@@ -0,0 +1,12 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
---
<WebsiteLayout title="Interner Server Fehler">
<div class="flex flex-col justify-center items-center gap-4 h-full w-full absolute">
<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">500: Interner Server Fehler</h1>
<p class="flex gap-2 text-center">
Es ist ein unerwarteter interner Server Fehler aufgetreten. Bitte versuche deine Anfrage erneut.
</p>
</div>
</WebsiteLayout>

View File

@@ -0,0 +1,16 @@
---
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import Admins from '@app/admin/admins/Admins.svelte';
import SidebarActions from '@app/admin/admins/SidebarActions.svelte';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { BASE_PATH } from 'astro:env/server';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Admin);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Website Admins">
<SidebarActions slot="actions" client:load />
<Admins client:load />
</AdminLayout>

View File

@@ -0,0 +1,16 @@
---
import { Session } from '@util/session';
import { Permissions } from '@util/permissions';
import { BASE_PATH } from 'astro:env/server';
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import SidebarActions from '@app/admin/strikeReasons/SidebarActions.svelte';
import StrikeReasons from '@app/admin/strikeReasons/StrikeReasons.svelte';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Reports);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Reports">
<SidebarActions slot="actions" client:load />
<StrikeReasons client:load />
</AdminLayout>

View File

@@ -0,0 +1,14 @@
---
import Feedback from '@app/admin/feedback/Feedback.svelte';
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { BASE_PATH } from 'astro:env/server';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Feedback);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Feedback">
<Feedback client:load />
</AdminLayout>

View File

@@ -0,0 +1,10 @@
---
import { Session } from '@util/session.ts';
import { BASE_PATH } from 'astro:env/server';
import AdminLayout from '@layouts/admin/AdminLayout.astro';
const session = Session.sessionFromCookies(Astro.cookies);
if (!session) return Astro.redirect(`${BASE_PATH}/admin/login`);
---
<AdminLayout title="Admin Panel" />

View File

@@ -0,0 +1,49 @@
---
import AdminLoginLayout from '@layouts/admin/AdminLoginLayout.astro';
import Password from '@components/input/Password.svelte';
import Input from '@components/input/Input.svelte';
import Popup from '@components/popup/Popup.svelte';
---
<AdminLoginLayout title="Login">
<div class="flex justify-center items-center w-full h-screen">
<div class="card w-96 px-6 py-6 shadow-lg">
<h1 class="text-3xl text-center">Admin Login</h1>
<div class="divider"></div>
<form id="login" class="flex flex-col items-center">
<div>
<Input id="username" type="text" label="Nutzername" required />
<Password id="password" label="Passwort" required />
</div>
<div class="mt-4">
<button class="btn btn-neutral">Login</button>
</div>
</form>
</div>
</div>
</AdminLoginLayout>
<Popup client:idle />
<script>
import { actions } from 'astro:actions';
import { popupState } from '@components/popup/Popup';
const loginForm = document.getElementById('login') as HTMLFormElement;
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
const usernameInput = document.getElementById('username') as HTMLInputElement;
const passwordInput = document.getElementById('password') as HTMLInputElement;
const { error } = await actions.session.login({
username: usernameInput.value,
password: passwordInput.value
});
if (error) {
popupState.set({ type: 'error', title: 'Fehler', message: error.message });
return;
}
window.location.href = 'admin';
});
</script>

View File

@@ -0,0 +1,27 @@
import type { APIRoute } from 'astro';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import fs from 'node:fs';
import path from 'node:path';
import { UPLOAD_PATH } from 'astro:env/server';
export const GET: APIRoute = async ({ params, cookies }) => {
Session.actionSessionFromCookies(cookies, Permissions.Reports);
if (!UPLOAD_PATH) return new Response(null, { status: 404 });
const fileHash = params.fileHash as string;
const filePath = path.join(UPLOAD_PATH, fileHash);
if (!fs.existsSync(filePath)) return new Response(null, { status: 404 });
const fileStat = fs.statSync(filePath);
const fileStream = fs.createReadStream(filePath);
return new Response(fileStream as any, {
status: 200,
headers: {
'Content-Length': fileStat.size.toString()
}
});
};

View File

@@ -0,0 +1,16 @@
---
import { Session } from '@util/session';
import { Permissions } from '@util/permissions';
import { BASE_PATH } from 'astro:env/server';
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import SidebarActions from '@app/admin/reports/SidebarActions.svelte';
import Reports from '@app/admin/reports/Reports.svelte';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Reports);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Reports">
<SidebarActions slot="actions" client:load />
<Reports client:load />
</AdminLayout>

View File

@@ -0,0 +1,17 @@
---
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import Settings from '@app/admin/settings/Settings.svelte';
import { db } from '@db/database.ts';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { BASE_PATH } from 'astro:env/server';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Settings);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
const settings = await db.getSettings({});
---
<AdminLayout title="Einstellungen">
<Settings {settings} client:load />
</AdminLayout>

View File

@@ -0,0 +1,14 @@
---
import { Session } from '@util/session';
import { Permissions } from '@util/permissions';
import { BASE_PATH } from 'astro:env/server';
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import Tools from '@app/admin/tools/Tools.svelte';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Tools);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Reports">
<Tools client:load />
</AdminLayout>

View File

@@ -0,0 +1,16 @@
---
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import BlockedUsers from '@app/admin/blockedUsers/BlockedUsers.svelte';
import SidebarActions from '@app/admin/blockedUsers/SidebarActions.svelte';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { BASE_PATH } from 'astro:env/server';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Users);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Blockierte Nutzer">
<SidebarActions slot="actions" client:load />
<BlockedUsers client:load />
</AdminLayout>

View File

@@ -0,0 +1,16 @@
---
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import Users from '@app/admin/users/Users.svelte';
import SidebarActions from '@app/admin/users/SidebarActions.svelte';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { BASE_PATH } from 'astro:env/server';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Users);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Registrierte Nutzer">
<SidebarActions slot="actions" client:load />
<Users client:load />
</AdminLayout>

89
src/pages/admins.astro Normal file
View File

@@ -0,0 +1,89 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import { Icon } from 'astro-icon/components';
const team = [
{
name: 'Elias',
nickname: 'MineTec',
roles: ['Gründer', 'Support', 'Organisation', 'Softwareentwicklung', 'Systemadministrator'],
links: [{ name: 'Website', href: 'https://mhsl.eu/aboutme/', icon: 'heroicons:globe-alt-solid' }]
},
{
name: 'Jannik',
nickname: 'Goldi187',
roles: ['Support', 'Organisation']
},
{
name: 'Adrian',
nickname: 'h0nny27',
roles: ['Support']
},
{
name: 'Ruben',
nickname: 'bytedream',
roles: ['Softwareentwicklung'],
links: [{ name: 'Website', href: 'https://bytedream.dev', icon: 'heroicons:globe-alt-solid' }]
},
{
name: 'Lars',
nickname: '28Pupsi28',
roles: ['Softwareentwicklung'],
links: [{ name: 'Website', href: 'https://mathemann.ddns.net/turtle_game/', icon: 'heroicons:globe-alt-solid' }]
},
{
name: 'Hanad',
nickname: 'Voldemort2212',
roles: ['Support', 'Mediengestaltung']
}
];
---
<WebsiteLayout title="Team">
<div class="m-auto flex flex-col justify-center items-center px-4 py-6 2xl:px-48 sm:py-12">
<h1 class="text-5xl mb-10">Die Admins</h1>
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-4 my-4 justify-center">
{
team.map((member) => (
<div class="card max-w-96 bg-base-200">
<div class="card-body px-4 py-6">
<div class="relative flex flex-col items-center">
<div class="avatar placeholder mb-2">
<div class="bg-neutral text-neutral-content w-24 rounded-xl">
<img
class="m-[7.5px]"
style="width: 85%; height: 85%"
src={`https://mc-heads.net/head/${member.nickname.toLowerCase()}`}
alt={member.name}
/>
</div>
</div>
<p class="text-center text-lg mb-1">
{member.name} · {member.nickname}
</p>
<p class="text-center text-sm font-light">{member.roles.join(' · ')}</p>
{member.links && (
<div class="absolute top-0 right-2 h-24 flex">
<div class="divider divider-horizontal mx-2" />
<div class="flex items-center">
{member.links.map((link) => (
<a
class="flex w-9 h-9 justify-center items-center border rounded-full border-base-content"
href={link.href}
target="_blank"
title={link.name}
>
<Icon name={link.icon} size={22} />
</a>
))}
</div>
</div>
)}
</div>
</div>
</div>
))
}
</div>
</div>
</WebsiteLayout>

37
src/pages/api/feedback.ts Normal file
View File

@@ -0,0 +1,37 @@
import { z } from 'astro:schema';
import type { APIRoute } from 'astro';
import { API_SECRET } from 'astro:env/server';
import { db } from '@db/database.ts';
import { BASE_PATH } from 'astro:env/server';
const postSchema = z.object({
event: z.string(),
title: z.string(),
users: z.array(z.string())
});
export const POST: APIRoute = async ({ request }) => {
if (API_SECRET && request.headers.get('authorization') !== `Basic ${API_SECRET}`) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await postSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const feedbacks = await db.addUserFeedbacks({
event: parsed.event,
title: parsed.title,
uuids: parsed.users
});
const response = feedbacks.map((feedback) => ({
uuid: feedback.uuid,
url: `${BASE_PATH}/feedback/${feedback.urlHash}`
}));
return new Response(JSON.stringify({ feedback: response }), { status: 200 });
};

33
src/pages/api/player.ts Normal file
View File

@@ -0,0 +1,33 @@
import { z } from 'astro:schema';
import type { APIRoute } from 'astro';
import { API_SECRET } from 'astro:env/server';
import { db } from '@db/database.ts';
const getSchema = z.object({
user: z.string()
});
export const GET: APIRoute = async ({ request }) => {
if (API_SECRET && request.headers.get('authorization') !== `Basic ${API_SECRET}`) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await getSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const user = await db.getUserByUuid({ uuid: parsed.user });
if (!user) return new Response(null, { status: 404 });
const strikes = await db.getStrikesByUserId({ userId: user.id });
return new Response(
JSON.stringify({
strikes: strikes.map((s) => ({ at: s.at.getTime(), weight: s.reason.weight }))
}),
{ status: 200 }
);
};

103
src/pages/api/report.ts Normal file
View File

@@ -0,0 +1,103 @@
import type { APIRoute } from 'astro';
import { z } from 'astro:schema';
import { API_SECRET } from 'astro:env/server';
import { db } from '@db/database.ts';
import { sendWebhook, WebhookAction } from '@util/webhook.ts';
const postSchema = z.object({
reporter: z.string(),
reported: z.string().nullable(),
reason: z.string()
});
export const POST: APIRoute = async ({ request }) => {
if (API_SECRET && request.headers.get('authorization') !== `Basic ${API_SECRET}`) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await postSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const reporter = await db.getUserByUuid({ uuid: parsed.reporter });
if (!reporter) return new Response(null, { status: 404 });
let reported = null;
if (parsed.reported) {
reported = await db.getUserByUuid({ uuid: parsed.reported });
if (!reported) return new Response(null, { status: 404 });
}
const report = await db.addReport({
reporterId: reporter.id,
reportedId: reported?.id,
reason: parsed.reason,
body: null
});
return new Response(JSON.stringify({ url: report.url }), { status: 200 });
};
const putSchema = z.object({
reporter: z.string().nullable(),
reported: z.string(),
reason: z.string(),
body: z.string().nullable(),
notice: z.string().nullable(),
statement: z.string().nullable(),
strike_reason_id: z.number()
});
export const PUT: APIRoute = async ({ request }) => {
if (API_SECRET && request.headers.get('authorization') !== `Basic ${API_SECRET}`) {
return new Response(null, { status: 401 });
}
let parsed;
try {
parsed = await putSchema.parseAsync(await request.json());
} catch (_) {
return new Response(null, { status: 400 });
}
const reported = await db.getUserByUuid({ uuid: parsed.reported });
if (!reported) return new Response(null, { status: 404 });
let reporter = null;
if (parsed.reporter) {
reporter = await db.getUserByUuid({ uuid: parsed.reporter });
if (!reporter) return new Response(null, { status: 404 });
}
await db.transaction(async (tx) => {
const report = await tx.addReport({
reporterId: reporter?.id,
reportedId: reported.id,
createdAt: new Date(),
reason: parsed.reason,
body: parsed.body
});
await tx.editReportStatus({
reportId: report.id,
notice: parsed.notice,
statement: parsed.statement,
status: 'closed'
});
await tx.editStrike({
reportId: report.id,
strikeReasonId: parsed.strike_reason_id
});
});
// send webhook in background
sendWebhook(WebhookAction.Strike, {
user: reported.uuid!
});
return new Response(null, { status: 200 });
};

220
src/pages/faq.astro Normal file
View File

@@ -0,0 +1,220 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import { PAYPAL_LINK, TEAMSPEAK_LINK, DISCORD_LINK, SERVER_IP, START_DATE } from 'astro:env/server';
const faq = [
{
section: 'Allgemein',
questions: [
{
title: 'Wie kann ich einen Admin kontaktieren?',
content: `<p>Einen Admin kannst du im Chat, über WhatsApp, per Teamspeak
<a class="link" href="${TEAMSPEAK_LINK}">mhsl.eu</a> oder Discord
<a class="link" href="${DISCORD_LINK}" target="_blank">${DISCORD_LINK}</a> kontaktieren.</p>`
},
{
title: 'Wer ist eigentlich Organisator und warum?',
content: `<p>Wir sind ein kleines Team von Minecraft-Enthusiasten, das bereits im siebten Jahr in Folge
Minecraft CraftAttack organisiert. Jedes Jahr arbeiten wir daran, das Spielerlebnis zu
verbessern und die Teilnehmerzahl zu steigern. Weitere Infos findest du auf der Teamseite.</p>`
},
{
title: 'Wie lange bleibt der Server online?',
content: `<p>Der Server wird traditionell so lange online bleiben, wie noch aktiv darauf gespielt wird.</p>`
},
{
title: 'Warum benötigt ihr meine Daten bei der Anmeldung?',
content: `<p>Deine Daten werden nur intern gespeichert und dienen den Admins rein zur Organisation
des Projekts.</p>`
},
{
title: 'Gibt es einen Teamspeak-Server?',
content: `<p>Ja, den offiziellen Teamspeak-Server erreichst du unter der IP
<a class="link" href="${TEAMSPEAK_LINK}">mhsl.eu</a>.</p>`
},
{
title: 'Gibt es einen Discord-Server?',
content: `<p>Ja, den offiziellen Discord-Server erreichst du unter <a class="link" href="${DISCORD_LINK}" target="_blank">${DISCORD_LINK}</a>.</p>`
},
{
title: 'Wozu dient die CraftAttack-WhatsApp-Gruppe?',
content: `<p>In der WhatsApp-Gruppe erhältst du alle wichtigen Infos bezüglich CraftAttack.</p>`
}
]
},
{
section: 'Anmeldung',
questions: [
{
title: 'Wann startet CraftAttack 8?',
content: `<p>Der Start von CraftAttack 8 findet gemeinsam am
${new Date(START_DATE).toLocaleDateString('de-DE', { year: 'numeric', month: 'numeric', day: 'numeric' })}
um ${new Date(START_DATE).toLocaleTimeString('de-DE', { hour: 'numeric', minute: 'numeric' })} Uhr statt.
Am besten bist du schon einige Minuten vorher auf dem Server. Natürlich kannst du aber auch danach jederzeit dazustoßen.
</p>`
},
{
title: 'Wer kann alles mitspielen?',
content: `<p>Jeder, der entweder Minecraft Java oder Bedrock (Handy und Konsole) besitzt und mindestens 6 Jahre
alt ist, kann mitspielen.</p>`
},
{
title: 'Wie kann ich mitspielen?',
content: `<p>Um mitzuspielen, musst du dich einfach hier auf der Website anmelden und der WhatsApp-Gruppe
beitreten.</p>`
},
{
title: 'Auf welcher Version läuft der Server?',
content: `<p>Gespielt wird immer auf der neuesten Version, also laut aktuellem Stand Version 1.21.4.</p>`
},
{
title: 'Kann ich auch als Bedrock-Spieler (Handy oder Konsole) mitspielen?',
content: `<p>Ja, auch als Bedrock-Spieler kannst du mitspielen, sofern du anderen Servern beitreten kannst.</p>`
},
{
title: 'Ich kann mich nicht anmelden, was kann ich tun?',
content: `<p>Wenn du dich nicht anmelden kannst, solltest du Folgendes überprüfen:</p>
<ol class="list-decimal pl-8 py-3">
<li>Ist dein Spielername korrekt geschrieben?</li>
<li>Hast du dich bereits angemeldet? Es ist nur ein Account pro Spieler erlaubt.</li>
<li>Hast du die richtige Spieledition ausgewählt?</li>
</ol>
<p>Falls du dich aus unerklärlichen Gründen trotzdem nicht anmelden kannst, kannst du
dich jederzeit beim Admin-Team melden.</p>`
},
{
title: 'Ich komme nicht auf den Server, was kann ich tun?',
content: `<p>Wenn du dem Server nicht beitreten kannst, überprüfe Folgendes:</p>
<ol class="list-decimal pl-8 py-3">
<li>Hast du die korrekte IP verwendet? Sie lautet <span class="underline italic">${SERVER_IP}</span>.</li>
<li>Hast du Leerzeichen verwendet, insbesondere vor oder hinter der IP, oder dich vertippt?</li>
<li>Kommst du auf andere Server, oder ist es nur ein Problem beim CraftAttack-Server?</li>
<li>Hast du dich korrekt auf der Webseite angemeldet?</li>
</ol>
<p>Falls du trotzdem nicht beitreten kannst, melde dich beim Admin-Team und halte die
Fehlermeldung bereit.</p>`
},
{
title: 'Was ist die Server-IP?',
content: `<p>Die Serveradresse lautet: <span class="underline italic">${SERVER_IP}</span>.</p>`
},
{
title: 'Ist es kostenlos mitzuspielen?',
content: `<p>Ja, die Teilnahme ist selbstverständlich kostenlos.${
PAYPAL_LINK
? ` Wir freuen uns aber, wenn du das Projekt mit einer Spende nach der Anmeldung unterstützen würdest.<br>
Hier kannst du für das Projekt spenden: <a class="link" href=${PAYPAL_LINK} target="_blank">${PAYPAL_LINK}</a>.`
: ''
}</p>`
},
{
title: 'Die Anmeldefrist ist vorbei, aber ich möchte mich trotzdem noch anmelden. Was kann ich tun?',
content: `<p>Generell solltest du dich immer während des Anmeldezeitraums anmelden. Falls die Anmeldung
allerdings bereits geschlossen ist, kannst du einen Admin kontaktieren, der dich im Fall der Fälle noch nachträglich
anmelden kann.</p>`
},
{
title: 'Ist ein 2. Account erlaubt?',
content: `<p>Nein, pro Teilnehmer ist nur ein Account zugelassen.</p>`
}
]
},
{
section: 'Ingame',
questions: [
{
title: 'Wo kann ich meinen Shop errichten?',
content: `<p>Generell darfst du Shops überall errichten, aber es bietet sich an, alle Shops in einem
Shopping-District nahe des Spawns anzusiedeln.</p>`
},
{
title: 'Sind Farmen erlaubt?',
content: `<p>Ja, Farmen sind generell erlaubt. Allerdings sind lag-erzeugende Maschinen, Farmen
(Zero-Tick-Farmen etc.) oder andere Bauten, die den Spielfluss stören könnten, verboten.</p>`
},
{
title: 'Was und wann sind Events?',
content: `<p>Abends, meist gegen 18 Uhr, finden gelegentlich Events statt, bei denen du Items gewinnen kannst
und in kleinen Minispielen gegen deine Mitspieler antrittst. Die genauen Abläufe siehst du, wenn du abends auf dem
Server bist.</p>`
},
{
title: 'Wo und wie kann ich einen Regelverstoß melden?',
content: `<p>Wenn du einen Regelverstoß melden willst, kannst du ingame den Befehl <code>/report</code> nutzen,
um einen Admin zu kontaktieren.</p>`
},
{
title: 'Was hat es mit dem Blutmond auf sich?',
content: `<p>Alle dreißig ingame-Tage solltest du nachts auf der Hut sein, denn die Monster sind in dieser Nacht
deutlich stärker als üblich, droppen aber auch besseren Loot.</p>`
},
{
title: 'Was hat es mit dem Vogelfrei-Modus auf sich?',
content: `<p>CraftAttack ist grundsätzlich ein friedliches Projekt. Falls du jedoch kein Problem damit hast,
angegriffen zu werden, kannst du dich mit <code>/vogelfrei</code> in den Vogelfrei-Modus setzen. Dadurch sehen andere
Spieler, dass du für einen Kampf offen bist. Der Vogelfrei-Modus kann allerdings erst nach einigen Stunden wieder
beendet werden.</p>`
},
{
title: 'Was hat es mit dem Rang „Langzeitspieler“ auf sich?',
content: `<p>Spieler, die seit über drei Jahren am Projekt teilnehmen, erhalten den Langzeitrang. Dieser wirkt
sich allerdings nicht auf das Spielgeschehen aus.</p>`
},
{
title: 'Was gibt es für neue Features?',
content: `<ul class="list-disc pl-8">
<li>Miniböcke, die du selbst gestalten kannst</li>
<li>Neue Event-Spiele</li>
<li>Einige Quality-of-Life-Features, die du mit <code>/settings</code> erreichst</li>
<li>Langzeitrang</li>
</ul>`
},
{
title: 'Wann wird das End geöffnet?',
content: `<p>Das End wird gemeinsam am 03.01.2025 um 19:00 Uhr geöffnet, und wir besiegen gemeinsam den
Enderdrachen.</p>`
},
{
title: 'Darf ich andere Spieler töten?',
content: `<p>Andere Spieler zu töten ist generell verboten. Wenn es jedoch nur zum Spaß und mit dem anderen
Spieler abgesprochen ist, haben wir nichts dagegen einzuwenden. Außerdem ist es erlaubt, vogelfreie Spieler zu
töten.</p>`
},
{
title: 'Welche Minecraft-Clients sind erlaubt?',
content: `<p>Jegliche Clientmodifikationen, die deutliche Vorteile gegenüber anderen Spielern bringen, sind
nicht gestattet.</p>`
}
]
}
];
---
<WebsiteLayout title="FAQ">
<div class="mx-4 my-6 sm:mx-24 sm:my-12">
<h1 class="text-3xl lg:text-5xl mb-16 text-center">FAQ</h1>
<div class="grid lg:grid-cols-2 2xl:grid-cols-3 gap-10">
{
faq.map((questions) => (
<div>
<h2 class="text-4xl text-center mb-3">{questions.section}</h2>
<div>
{questions.questions.map((question) => (
<>
<div class="collapse collapse-arrow">
<input type="checkbox" autocomplete="off" />
<div class="collapse-title">{question.title}</div>
<div class="collapse-content">
<div class="ml-2" set:html={question.content} />
</div>
</div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" />
</>
))}
</div>
</div>
))
}
</div>
</div>
</WebsiteLayout>

123
src/pages/feedback.astro Normal file
View File

@@ -0,0 +1,123 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import ConfirmPopup from '@components/popup/ConfirmPopup.svelte';
import Popup from '@components/popup/Popup.svelte';
import Select from '@components/input/Select.svelte';
import Textarea from '@components/input/Textarea.svelte';
import Input from '@components/input/Input.svelte';
---
<WebsiteLayout title="Feedback & Kontakt">
<div class="flex justify-center items-center">
<div class="mt-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg">
<h2 class="text-3xl text-center">Feedback & Kontakt</h2>
<form id="feedback-contact">
<div class="space-y-4 mt-6 mb-4">
<Select id="type" values={{ 'website-feedback': 'Feedback', 'website-contact': 'Kontakt' }} />
<Textarea id="content" label="Feedback" dynamicWidth required rows={5} />
<Input id="email" type="email" label="Email" required hidden />
</div>
<button id="send" class="btn">Senden</button>
</form>
</div>
</div>
</WebsiteLayout>
<ConfirmPopup client:idle />
<Popup client:idle />
<script>
import { actions } from 'astro:actions';
import { confirmPopupState } from '@components/popup/ConfirmPopup';
import { popupState } from '@components/popup/Popup';
import { actionErrorPopup } from '../util/action';
function setupForm() {
const form = document.getElementById('feedback-contact') as HTMLFormElement;
const type = document.getElementById('type') as HTMLSelectElement;
const content = document.getElementById('content') as HTMLTextAreaElement;
const email = document.getElementById('email') as HTMLInputElement;
// reset form on site (re-)load
form.reset();
type.addEventListener('change', () => {
if (type.value === 'website-feedback') {
// content input
content.previousElementSibling!.firstChild!.textContent = 'Feedback';
// email input
email.parentElement!.hidden = true;
email.required = false;
} else if (type.value === 'website-contact') {
// content input
content.previousElementSibling!.firstChild!.textContent = 'Anfrage';
// email input
email.required = true;
email.parentElement!.hidden = false;
}
});
email.required = false;
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (type.value === 'website-feedback') {
confirmPopupState.set({
title: 'Feedback abschicken',
message: 'Soll das Feedback abgeschickt werden?',
onConfirm: () => sendFeedback().then(() => form.reset())
});
} else if (type.value === 'website-contact') {
confirmPopupState.set({
title: 'Kontaktanfrage abschicken',
message: 'Soll die Kontaktanfrage abgeschickt werden?',
onConfirm: () => sendContact().then(() => form.reset())
});
}
});
const sendFeedback = async () => {
const { error } = await actions.feedback.addWebsiteFeedback({
content: content.value
});
if (error) {
actionErrorPopup(error);
return;
}
popupState.set({
type: 'info',
title: 'Feedback abgeschickt',
message: 'Dein Feedback wurde abgeschickt. Vielen Dank, dass du uns hilfst, das Projekt besser zu machen!'
});
};
const sendContact = async () => {
const { error } = await actions.feedback.addWebsiteContact({
content: content.value,
email: email.value
});
if (error) {
actionErrorPopup(error);
return;
}
popupState.set({
type: 'info',
title: 'Kontaktanfrage abgeschickt',
message:
'Deine Kontaktanfrage wurde abgeschickt. Jemand aus dem Admin-Team wird sich nächstmöglich bei Dir melden.'
});
};
}
const pathname = document.location.pathname;
document.addEventListener('astro:page-load', () => {
if (document.location.pathname !== pathname) return;
setupForm();
});
</script>

View File

@@ -0,0 +1,66 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import Input from '@components/input/Input.svelte';
import Textarea from '@components/input/Textarea.svelte';
import { db } from '@db/database.ts';
const { urlHash } = Astro.params;
const feedback = urlHash ? await db.getFeedbackByUrlHash({ urlHash: urlHash }) : null;
if (!feedback) {
return new Response(null, { status: 404 });
}
---
<WebsiteLayout title="Feedback">
<div class="flex justify-center items-center">
<div class="mt-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg">
<h2 class="text-3xl text-center">Feedback</h2>
<form id="feedback" data-url-hash={urlHash}>
<div class="space-y-4 mt-6 mb-4">
<Input value={feedback.title} label="Event" dynamicWidth readonly />
<Textarea
id="content"
value={feedback.content}
label="Feedback"
rows={10}
dynamicWidth
required
readonly={feedback.content !== null}
/>
</div>
<button id="send" class="btn" disabled>Feedback senden</button>
</form>
</div>
</div>
</WebsiteLayout>
<script>
import { actions } from 'astro:actions';
import { actionErrorPopup } from '@util/action';
document.addEventListener('astro:page-load', () => {
const form = document.getElementById('feedback') as HTMLFormElement;
const content = document.getElementById('content') as HTMLTextAreaElement;
const sendButton = document.getElementById('send') as HTMLButtonElement;
form.addEventListener('submit', async (e) => {
e.preventDefault();
const { error } = await actions.feedback.submitFeedback({
urlHash: form.dataset.urlHash!,
content: content.value
});
if (error) {
actionErrorPopup(error);
return;
}
content.readOnly = true;
sendButton.disabled = true;
});
content.addEventListener('input', () => (sendButton.disabled = content.value === '' || content.readOnly));
});
</script>

114
src/pages/index.astro Normal file
View File

@@ -0,0 +1,114 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import Countdown from '@app/website/index/Countdown.svelte';
import Craftattack from '@assets/img/craftattack.webp';
import Background from '@assets/img/background.webp';
import { START_DATE, YOUTUBE_INTRO_LINK } from 'astro:env/server';
import { getSetting, SettingKey } from '@util/settings';
import { db } from '@db/database.ts';
const signupEnabled = await getSetting(db, SettingKey.SignupEnabled, false);
const signupInfoMessage = await getSetting(db, SettingKey.SignupInfoMessage);
const information = [
{
title: 'Das Projekt',
description:
'CraftAttack ist ein Vanilla-Minecraft-Projekt, bei dem zahlreiche Spieler im friedlichen Miteinander spielen. Von gemeinsamen Bauvorhaben bis hin zum kollektiven Kampf gegen den Enderdrachen können die vielfältigen Aspekte von Minecraft erkundet werden.'
},
{
title: 'Events',
description:
'Abwechslungsreiche Events und verschiedene Minispiele sorgen dafür, dass es nie langweilig wird und garantieren somit jede Menge Spielspaß.'
},
{
title: 'Voraussetzungen',
description:
'Jeder ist willkommen und kann mitspielen. Dazu benötigst Du nur einen Minecraft-Account und schon bist Du Teil unser Community :)'
}
];
---
<WebsiteLayout title="CraftAttack 8">
<div class="bg-base-100 flex flex-col min-h-screen relative">
<div class="flex items-center xl:w-1/2 px-6 sm:px-10 min-h-screen h-full">
<div class="flex flex-col w-full xl:h-3/4 my-10">
<img src={Craftattack.src} alt="CraftAttack 8" />
<div class="flex flex-col w-full mt-2 lg:mt-5 lg:w-10/12 h-full">
<div>
<div class="divider"></div>
<div class="flex flex-col md:flex-row xl:flex-col gap-5">
{
information.map((info) => (
<div>
<h4 class="mb-1 font-bold">{info.title}</h4>
<p>{info.description}</p>
</div>
))
}
</div>
<div class="divider"></div>
</div>
<div class="flex justify-center">
<a class="btn btn-outline btn-accent hover:bg-white" href="signup"
>{signupEnabled ? 'Jetzt registrieren' : 'Infos zur Anmeldung'}</a
>
</div>
{signupInfoMessage && <span class="text-center text-xs text-base-content/80 mt-3">{signupInfoMessage}</span>}
</div>
</div>
</div>
<div
class="hidden xl:block absolute top-0 left-0 h-full w-full"
style="clip-path: polygon(60% 0, 100% 0, 100% 100%, 40% 100%);"
>
<img src={Background.src} alt="" loading="lazy" width="100%" height="100%" />
</div>
<div class="hidden xl:flex justify-center absolute bottom-12 right-0 w-[60%]">
<Countdown end={Date.parse(START_DATE)} client:load />
</div>
</div>
{
YOUTUBE_INTRO_LINK && (
<div class="bg-base-300 w-full py-12 flex justify-center">
<iframe
width="624"
height="351"
src={YOUTUBE_INTRO_LINK}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
/>
</div>
)
}
<div class="flex justify-center py-20 bg-base-200">
<div class="card bg-base-100 shadow-lg w-11/12 xl:w-5/12 p-10">
<div>
<h2 class="text-3xl text-black dark:text-white mb-8">Über uns</h2>
<p>
Wir sind ein kleines <a class="link" href="team">Team</a> von Minecraft-Enthusiasten, das bereits im 8. Jahr in
Folge Minecraft CraftAttack organisiert. Jahr für Jahr arbeiten wir daran, das Spielerlebnis zu verbessern und
steigeren die Teilnehmerzahl.
</p>
<p>
Unser Ziel bei diesem ab dem <span class="italic"
>{
new Date(START_DATE).toLocaleString('de-DE', {
day: '2-digit',
month: 'numeric',
year: 'numeric'
})
}</span
>
stattfindenden Projekts ist es, sicherzustellen, dass alle Spieler eine großartige Erfahrung haben und alles reibungslos
abläuft. Wir freuen uns immer über Anregungen und stehen Dir jederzeit zur Verfügung.
</p>
</div>
</div>
</div>
</WebsiteLayout>

View File

@@ -0,0 +1,23 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import { db } from '@db/database.ts';
import Draft from './_draft.astro';
import Submitted from './_submitted.astro';
import Popup from '@components/popup/Popup.svelte';
import ConfirmPopup from '@components/popup/ConfirmPopup.svelte';
const { urlHash } = Astro.params;
const report = urlHash ? await db.getReportByUrlHash({ urlHash: urlHash }) : null;
if (!report) {
return new Response(null, { status: 404 });
}
---
<WebsiteLayout title="Report">
{report.createdAt === null ? <Draft report={report} /> : <Submitted report={report} />}
</WebsiteLayout>
<Popup client:idle />
<ConfirmPopup client:idle />

View File

@@ -0,0 +1,106 @@
---
import type { db } from '@db/database.ts';
import AdversarySearch from '@app/website/report/AdversarySearch.svelte';
import Dropzone from '@app/website/report/Dropzone.svelte';
import Input from '@components/input/Input.svelte';
import Textarea from '@components/input/Textarea.svelte';
import { MAX_UPLOAD_BYTES } from 'astro:env/server';
interface Props {
report: Awaited<ReturnType<db.getReportByUrlHash>>;
}
const { report } = Astro.props;
---
<div class="flex justify-center items-center">
<div class="mt-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg">
<h2 class="text-3xl text-center">
Report von <span class="underline">{report.reporter.username}</span> gegen <span
id="adversary-username"
class="underline">{report.reported?.username ?? 'unbekannt'}</span
>
</h2>
<form id="report" data-url-hash={report.urlHash} data-adversary-username={report.reported?.username}>
<div class="space-y-4 my-4">
<div class="flex flex-col gap-4">
<AdversarySearch client:load />
<Input id="reason" value={report.reason} label="Report Grund" dynamicWidth required />
<Textarea id="body" value={report.body} label="Details" rows={10} dynamicWidth required />
<Dropzone maxFilesBytes={MAX_UPLOAD_BYTES} client:load />
</div>
<button id="send" class="btn" disabled={report.body}>Report senden</button>
</div>
</form>
</div>
</div>
<script>
import { actions } from 'astro:actions';
import { actionErrorPopup } from '@util/action';
import { popupState } from '@components/popup/Popup';
document.addEventListener('astro:page-load', () => {
const eventCancelController = new AbortController();
document.addEventListener('astro:after-swap', () => eventCancelController.abort());
const adversary = document.getElementById('adversary-username') as HTMLSpanElement;
const form = document.getElementById('report') as HTMLFormElement;
const reason = document.getElementById('reason') as HTMLInputElement;
const body = document.getElementById('body') as HTMLTextAreaElement;
const sendButton = document.getElementById('send') as HTMLButtonElement;
let adversaryUsername = form.dataset.adversaryUsername ?? null;
let attachments: File[] = [];
body.addEventListener('change', () => {
sendButton.disabled = !body.value;
});
document.addEventListener(
'adversaryInput',
(e: any & { detail: { adversaryUsername: string | null } }) => {
adversaryUsername = e.detail.adversaryUsername;
adversary.textContent = e.detail.adversaryUsername ?? 'unbekannt';
},
{ signal: eventCancelController.signal }
);
document.addEventListener(
'dropzoneInput',
(e: any & { detail: { files: File[] } }) => {
attachments = e.detail.files;
},
{ signal: eventCancelController.signal }
);
form.addEventListener(
'submit',
async (e) => {
e.preventDefault();
const formData = new FormData();
formData.set('urlHash', form.dataset.urlHash!);
if (adversaryUsername != null) formData.set('reported', adversaryUsername);
formData.set('reason', reason.value);
formData.set('body', body.value);
for (const attachment of attachments) {
formData.append('files', attachment);
}
const { error } = await actions.report.submitReport(formData);
if (error) {
actionErrorPopup(error);
return;
}
popupState.set({
type: 'info',
title: 'Report abgeschickt',
message: 'Der Report wurde abgeschickt. Ein Admin wird sich schnellstmöglich darum kümmern.',
onClose: () => location.reload()
});
},
{ signal: eventCancelController.signal }
);
});
</script>

View File

@@ -0,0 +1,33 @@
---
import type { db } from '@db/database.ts';
import Textarea from '@components/input/Textarea.svelte';
interface Props {
report: Awaited<ReturnType<db.getReportByUrlHash>>;
}
const { report } = Astro.props;
---
<div class="flex justify-center items-center">
<div class="mt-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg">
{
report.status?.status == null ? (
<p>Dein Report wird in kürze bearbeitet</p>
) : report.status?.status === 'open' ? (
<p>Dein Report befindet sich in Bearbeitung</p>
) : (
<>
<p>Dein Report wurde bearbeitet</p>
<Textarea
value={report.status?.statement}
label="Antwort vom Admin Team (optional)"
rows={5}
dynamicWidth
readonly
/>
</>
)
}
</div>
</div>

37
src/pages/rules.astro Normal file
View File

@@ -0,0 +1,37 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import { rules } from '../rules';
---
<WebsiteLayout title="Regeln">
<div class="mx-4 my-6 sm:mx-48 sm:my-12">
<h1 class="text-3xl lg:text-5xl mb-4">CraftAttack 8 Regelwerk</h1>
<div class="collapse collapse-arrow">
<input type="checkbox" autocomplete="off" checked />
<div class="collapse-title">
<p>0. Vorwort</p>
</div>
<div class="collapse-content">
<p>{rules.header}</p>
<p class="mt-1 text-[.75rem]">{rules.footer}</p>
</div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
</div>
{
rules.sections.map((section, i) => (
<div>
<div class="collapse collapse-arrow">
<input type="checkbox" autocomplete="off" />
<div class="collapse-title">
<p>
{i + 1}. {section.title}
</p>
</div>
<div class="collapse-content" set:html={section.content} />
</div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" />
</div>
))
}
</div>
</WebsiteLayout>

239
src/pages/signup.astro Normal file
View File

@@ -0,0 +1,239 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import Checkbox from '@components/input/Checkbox.svelte';
import Input from '@components/input/Input.svelte';
import Select from '@components/input/Select.svelte';
import RulesPopup from '@app/website/signup/RulesPopup.svelte';
import Popup from '@components/popup/Popup.svelte';
import RegisteredPopup from '@app/website/signup/RegisteredPopup.svelte';
import { getSettings, SettingKey } from '@util/settings';
import { db } from '@db/database.ts';
import { DISCORD_LINK, PAYPAL_LINK, START_DATE, TEAMSPEAK_LINK } from 'astro:env/server';
const signupSetting = await getSettings(db, [
SettingKey.SignupEnabled,
SettingKey.SignupDisabledMessage,
SettingKey.SignupDisabledSubMessage
]);
const signupEnabled = signupSetting[SettingKey.SignupEnabled] ?? false;
const signupDisabledMessage = signupSetting[SettingKey.SignupDisabledMessage] ?? 'Anmeldung deaktiviert';
const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessage] ?? '';
---
<WebsiteLayout title="Anmeldung" footer={false}>
<div
class="flex justify-center w-full min-h-screen bg-base-200"
class:list={[!signupEnabled ? 'max-h-screen overflow-hidden' : undefined]}
>
<div class="relative grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 my-12 bg-base-100 shadow-lg h-min">
<h1 class="text-center text-3xl lg:text-5xl">Anmeldung</h1>
<form id="signup">
<div class="divider">Persönliche Angaben</div>
<div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-x-4">
<Input
id="firstname"
type="text"
label="Vorname"
required
validation={{
pattern: '^\\p{L}{2,}',
hint: 'Bitte gib Deinen vollständigen Vornamen an, dieser muss mindestens aus 2 Zeichen bestehen.'
}}
dynamicWidth
/>
<Input
id="lastname"
type="text"
label="Nachname"
required
validation={{
pattern: '^\\p{L}{2,}',
hint: 'Bitte gib Deinen vollständigen Nachnamen an, dieser muss mindestens aus 2 Zeichen bestehen.'
}}
dynamicWidth
/>
<Input
id="birthday"
type="date"
label="Geburtstag"
required
validation={{
max: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365 * 6).toLocaleDateString('sv-SE'),
hint: 'Bitte gib Deinen vollständigen Geburtstag und die korrekte Jahreszahl an. Du musst mindestens 6 Jahre alt sein.'
}}
dynamicWidth
>
<span slot="notice">Die Angabe hat keine Auswirkungen auf das Spielgeschehen.</span>
</Input>
<Input
id="phone"
type="tel"
label="Telefonnummer"
validation={{
pattern: '^[+\\(\\)\\s\\d]+$',
hint: 'Bitte gib Deine vollständige Telefonnummer an, diese darf keine ungültigen Zeichen enthalten'
}}
dynamicWidth
>
<span slot="notice">
Diese nutzen wir, um Dich in der Whatsapp-Gruppe zuzuordnen und kontaktieren zu können.
<br />
<b>Die Angabe ist freiwillig, hilft den Administratoren jedoch sehr!</b>
</span>
</Input>
</div>
<div class="divider">Spiel</div>
<div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-x-4">
<Input id="username" type="text" label="Minecraft-Spielername" required dynamicWidth />
<Select
id="edition"
values={{ java: 'Java (PC)', bedrock: 'Bedrock (Konsole und Handys)' }}
label="Edition"
required
dynamicWidth
/>
</div>
<div class="divider"></div>
<div class="mx-2 grid gap-y-3 mb-6">
<Checkbox id="checkbox" required>
<span slot="label">
Ich bin mit der Speicherung meiner in der Anmeldung angegebenen, persönlichen Daten einverstanden. Siehe <a
class="link"
href="https://mhsl.eu/id.html"
target="_blank">Datenschutz</a
>
</span>
</Checkbox>
<Checkbox id="logs" required>
<span slot="label">
Ich bin mit der Speicherung in Form von Logs aller meiner, beim Spielen anfallenden, persönlichen Daten
durch den Server einverstanden
</span>
<span slot="notice" class="text-[.65rem]">
Dies betrifft jede Interaktion im Spiel und zugehörige Daten wie z.B. Chatnachrichten welche vom Minecraft
Client an den Server übermittelt werden
</span>
</Checkbox>
<Checkbox id="rules" required>
<span slot="label">
Ich bin mit den <a class="link" onclick="">Regeln</a> einverstanden und achte sie
</span>
</Checkbox>
</div>
<button class="btn btn-neutral">Anmeldung absenden</button>
</form>
</div>
</div>
</WebsiteLayout>
<RulesPopup client:idle />
<Popup client:idle />
<RegisteredPopup
client:idle
discordLink={DISCORD_LINK}
paypalLink={PAYPAL_LINK}
teamspeakLink={TEAMSPEAK_LINK}
startDate={START_DATE}
/>
{
!signupEnabled && (
<div class="absolute top-0 left-0 w-full h-full bg-black/50 backdrop-blur-sm z-10 rounded-xl flex justify-center items-center flex-col pt-20 lg:pt-0 text-2xl sm:text-4xl">
<h1>{signupDisabledMessage}</h1>
<h3>{signupDisabledSubMessage}</h3>
</div>
)
}
<script>
import { actions } from 'astro:actions';
import { popupState } from '@components/popup/Popup';
import { rulesPopupState, rulesPopupRead } from '@app/website/signup/RulesPopup';
import { registeredPopupState } from '@app/website/signup/RegisteredPopup';
function setupClientValidation() {
const rulesCheckbox = document.getElementById('rules') as HTMLInputElement;
const rulesCheckboxRulesLink = rulesCheckbox.nextElementSibling!.querySelector('.link') as HTMLAnchorElement;
// add popup state subscriber to check when the accepted button is clicked
rulesPopupState.subscribe((value) => {
if (value == 'accepted') rulesCheckbox.checked = true;
});
// add click handler to open rules popup to rules checkbox
rulesCheckbox.addEventListener('click', (e) => {
if (!rulesPopupRead.get()) {
e.preventDefault();
rulesPopupState.set('open');
}
});
// add click handler to open rules popup when clicking the rules link in the rules checkbox label
rulesCheckboxRulesLink!.addEventListener('click', () => rulesPopupState.set('open'));
}
function setupForm() {
const form = document.getElementById('signup')! as HTMLFormElement;
// reset form on site (re-)load
form.reset();
const sendSignup = async () => {
const { error } = await actions.signup.signup({
firstname: form.firstname.value,
lastname: form.lastname.value,
birthday: form.birthday.value,
phone: form.phone.value,
username: form.username.value,
edition: form.edition.value
});
if (error) {
if (error.code == 'CONFLICT' || error.code == 'FORBIDDEN') {
popupState.set({
type: 'error',
title: 'Fehler',
message: error.message
});
} else {
popupState.set({
type: 'error',
title: 'Fehler',
message: error.message
});
}
return;
}
registeredPopupState.set({
firstname: form.firstname.value,
lastname: form.lastname.value,
birthday: form.birthday.value,
phone: form.phone.value,
username: form.username.value,
edition: form.edition.value[0].toUpperCase() + form.edition.value.slice(1)
});
const cancel = registeredPopupState.subscribe((value) => {
if (value) return;
cancel();
form.reset();
});
};
form.addEventListener('submit', (e) => {
e.preventDefault();
sendSignup();
});
}
const pathname = document.location.pathname;
document.addEventListener('astro:page-load', () => {
if (document.location.pathname !== pathname) return;
setupClientValidation();
setupForm();
});
</script>