initial commit
Some checks failed
deploy / build-and-deploy (push) Failing after 21s

This commit is contained in:
2025-05-18 13:16:20 +02:00
commit 60f3f8a096
148 changed files with 17900 additions and 0 deletions

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/client';
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,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/client';
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/client';
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,50 @@
---
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 { BASE_PATH } from 'astro:env/client';
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 = `${BASE_PATH}/admin`;
});
</script>

View File

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

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/client';
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 });
};

View File

@ -0,0 +1,44 @@
import type { APIRoute } from 'astro';
import { API_SECRET } from 'astro:env/server';
import { z } from 'astro:schema';
import { db } from '@db/database.ts';
const postSchema = z.object({
user: z.string(),
killer: z.string().nullable(),
message: 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 });
}
let users;
if (parsed.killer) {
users = await db.getUsersByUuid({ uuid: [parsed.user, parsed.killer] });
} else {
users = await db.getUsersByUuid({ uuid: [parsed.user] });
}
const user = users[parsed.user];
const killer = parsed.killer ? users[parsed.killer] : null;
if (!user || (!killer && parsed.killer)) {
return new Response(null, { status: 404 });
}
await db.addDeath({
message: parsed.message,
deadUserId: user.id,
killerUserId: killer?.id
});
return new Response(null, { status: 200 });
};

View File

@ -0,0 +1,74 @@
import type { APIRoute } from 'astro';
import { db } from '@db/database.ts';
import { API_SECRET } from 'astro:env/server';
import { z } from 'astro:schema';
const postSchema = z.object({
user: 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 user = (await db.getUsersByUuid({ uuid: [parsed.user] }))[parsed.user];
if (user == null) {
return new Response(null, { status: 404 });
}
const team = await db.getTeamByUserUuid({ uuid: parsed.user });
if (team == null) {
return new Response(null, { status: 404 });
}
const death = await db.getDeathByUserId({ userId: user.id });
const strikes = await db.getStrikesByTeamId({ teamId: team.team.id });
const response = {
team: {
name: team.team.name,
color: team.team.color
},
dead: death != null,
strikeWeight: strikes.map((strike) => strike.reason.weight).reduce((a, b) => a + b, 0),
lastJoined: team.team.lastJoined ? new Date(team.team.lastJoined).getTime() : null
};
return new Response(JSON.stringify(response));
};
const putSchema = z.object({
user: z.string(),
lastJoined: 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 team = await db.getTeamByUserUuid({ uuid: parsed.user });
if (team == null) {
return new Response(null, { status: 404 });
}
await db.editTeam({
id: team.team.id,
lastJoined: new Date(parsed.lastJoined).toISOString()
});
return new Response(null, { status: 200 });
};

View File

@ -0,0 +1,26 @@
import type { APIRoute } from 'astro';
import { db } from '@db/database.ts';
import { API_SECRET } from 'astro:env/server';
export const GET: APIRoute = async ({ request }) => {
if (API_SECRET && request.headers.get('authorization') !== `Basic ${API_SECRET}`) {
return new Response(null, { status: 401 });
}
const teams = await db.getTeams({});
const response = [];
for (const team of teams) {
const users = [];
if (team.memberOne.uuid) users.push(team.memberOne.uuid);
if (team.memberTwo.uuid) users.push(team.memberTwo.uuid);
response.push({
name: team.name,
color: team.color,
users: users
});
}
return new Response(JSON.stringify(response));
};

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

@ -0,0 +1,214 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import { PAYPAL_LINK, TEAMSPEAK_LINK, DISCORD_LINK, SERVER_IP } from 'astro:env/client';
const faq = [
{
section: 'Anmeldung',
questions: [
{
title: 'Wann startet Varo 5?',
content: `<p>Der Start von Varo 5 findet gemeinsam am 23.06.2025 um 19:00 Uhr statt. Am besten bist du schon
einige Minuten vorher auf dem Server. Denk daran: Nur wenn du und dein Teampartner beide anwesend seid, könnt ihr am
Start teilnehmen.</p>`
},
{
title: 'Was tun, wenn ich am Starttermin nicht kann?',
content: `<p>Wenn du oder dein Teampartner am 23.06.2025 um 19:00 Uhr verhindert seid, könnt ihr einen Tag
später, also am 24.06.2025 ab 17:00 Uhr einsteigen. Die 30 Minuten von Tag 1 werden euch aus Fairness-Gründen allerdings
nicht gutgeschrieben.</p>`
},
{
title: 'Wer kann alles mitspielen?',
content: `<p>Jeder, der Minecraft Java besitzt, mindestens 6 Jahre alt ist und einen Teampartner hat, 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. Nur wenn dein Team vollständig ist, also aus zwei Spielern besteht, kannst du teilnehmen.</p>`
},
{
title: 'Auf welcher Version läuft der Server und was ist der Schwierigkeitsgrad?',
content: `<p>Gespielt wird in der aktuellsten Minecraft Java Version mit dem Schwierigkeitsgrad „Normal“.</p>`
},
{
title: 'Kann ich auch als Bedrock-Spieler (Handy oder Konsole) mitspielen?',
content: `<p>Nein, als Bedrock-Spieler kannst du bei Varo leider nicht mitspielen. Das gilt auch, falls du einen
Proxie betreibst, der Bedrock zu Java proxiet.</p>`
},
{
title: 'Ich wurde in CraftAttack gebannt, kann ich bei Varo trotzdem mitspielen?',
content: `<p>In Einzelfällen sind in CraftAttack gebannte Spieler auch von Varo ausgeschlossen. Diese können
sich dann erst gar nicht anmelden.</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><p>1. Ist dein Spielername
korrekt geschrieben?<br>2. Hast du dich bereits angemeldet? Es ist nur ein Account pro Spieler erlaubt.</p><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="italic">varo.mhsl.eu</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 Varo-Server?</li>
<li>Hast du dich korrekt auf der Webseite angemeldet?</li>
</ol>
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="italic">${SERVER_IP}</span></p>`
},
{
title: 'Ist es kostenlos mitzuspielen?',
content: `<p>Ja, die Teilnahme ist selbstverständlich kostenlos. Wir freuen uns aber, wenn du das Projekt mit
einer Spende nach der Anmeldung unterstützen würdest. 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>Eine nachträgliche Anmeldung ist ausnahmslos nicht möglich.</p>`
},
{
title: 'Ist ein 2. Account erlaubt?',
content: `<p>Nein, pro Teilnehmer ist nur ein Account zugelassen.</p>`
}
]
},
{
section: 'Anderes',
questions: [
{
title: 'Wie viel Spielzeit habe ich pro Tag?',
content: `<p>Sobald du den Server betritt müssen du und dein Teampartner 30 Minuten lang am Stück auf dem Server
spielen. Spielt dein Team an einem Tag nicht, dürft ihr am Folgetag 60 Minuten, also zwei 30-Minuten-Slots spielen. Pro
Tag wird deinem Team also eine Zeit von 30 Minuten gutgeschrieben. Sollte dein Team mehr als 90 Minuten Spielzeit
angesammelt haben, wird es automatisch vom Projekt ausgeschlossen.</p>`
},
{
title: 'Von wann bis wann ich dem Server täglich online?',
content: `<p>Du kannst dem Server zu Beginn täglich von 17:00 Uhr bis 21:00 Uhr joinen.</p>`
},
{
title: 'Gibt es eine Finale und wie läuft es ab?',
content: `<p>Es gibt planmäßig kein öffentliches Finale, aber natürlich wirst du am Ende über das Siegerteam
informiert.</p>`
},
{
title: 'Kann ich meinen Teampartner nach dem Tod spectaten?',
content: `<p>Nein, diese Möglichkeit besteht nicht. Er kann dir aber natürlich gerne seinen Bildschirm
übertragen.</p>`
},
{
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, die neben Minecraft CraftAttack dieses Jahr
das Sommerprojekt Varo organisieren. Weitere Infos findest du auf der Teamseite.</p>`
},
{
title: 'Spielen die Admins auch mit?',
content: `<p>Nein, alle Varo Admins sind keine Projektteilnehmer und sind lediglich für die Organisation, die
Kontrolle der Regeln und der technische Umsetzung zuständig. Falls Euch Admin-Namen aus CraftAttack bekannt sind, keine
Sorge: Das Adminteam von CraftAttack und Varo ist nicht zwingend identisch!</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 Varo-WhatsApp-Gruppe?',
content: `<p>In der WhatsApp-Gruppe erhältst du alle wichtigen Infos bezüglich Varo.</p>`
}
]
},
{
section: 'Ingame',
questions: [
{
title: 'Gibt es eine Worldborder?',
content: `<p>Ja, es gibt außerdem eine Worldborder, die sich mit der Zeit außerhalb der Spielzeiten langsam
verkleinert. Du wirst im Spiel über alles wichtige informiert, sobald du dich der Border gefährlich näherst.</p>`
},
{
title: 'Kann ich ein eigenes Netherportal bauen?',
content: `<p>Nein, das einzige Netherportal steht am Spawn. Weitere können nicht eröffnet werden.</p>`
},
{
title: 'Wie kann ich einen Report erstellen?',
content: `<p>Neben der Kontrolle durch die Admins kann mit /report ein Report erstellt werden, um Regelverstöße
zu melden. Ohne feste Belege, also einer Bildschirmaufnahme, ist ein Report bei Ingameverstößen leider nutzlos.</p>`
},
{
title: 'Welche Spielinhalte (Items etc.) sind deaktiviert?',
content: `<ol class="list-decimal pl-8 py-3">
<li>alle Netherite Items</li>
<li>alle Tränke der Stufe 2, ausgenommen Direktheilung 2</li>
<li>Totems</li>
<li>Enderkristalle</li>
<li>Mace</li>
<li>OP-Goldapfel</li>
<li>Villager Handel mit dem Bibliothekar und Panzermacher</li>
</ol>`
},
{
title: 'Warum sind diese Spielinhalte deaktiviert?',
content: `<p>Unser Ziel ist es, ein großartiges Projekt für alle Beteiligten auf die Beine zu stellen. Varo
haben wir in dieser Form und Größe noch nie veranstaltet auch für uns ist es also ein neues Kapitel, bei dem wir
wertvolle Erfahrungen sammeln wollen.<br>Wir haben die Regeln nach bestem Wissen und Gewissen so aufgestellt, um das
bestmögliche Spielerlebnis schaffen. Trotzdem ist uns dein Feedback wichtig: Nach dem Projekt holen wir deine Meinung
ein, um Varo und auch unsere künftigen Projekte nachhaltig weiterzuentwickeln.</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>

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

@ -0,0 +1,119 @@
---
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';
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;
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())
});
}
});
async function sendFeedback() {
const { error } = await actions.feedback.addWebsiteFeedback({
type: 'website-feedback',
content: content.value
});
if (error) {
popupState.set({
type: 'error',
title: 'Fehler beim senden des Feedbacks',
message: error.message
});
throw error;
}
popupState.set({
type: 'info',
title: 'Feedback abgeschickt',
message: 'Dein Feedback wurde abgeschickt. Vielen Dank, dass du uns hilfst, das Projekt besser zu machen!'
});
}
async function sendContact() {
const { error } = await actions.feedback.addWebsiteContact({
type: 'website-contact',
content: content.value,
email: email.value
});
if (error) {
popupState.set({
type: 'error',
title: 'Fehler beim senden der Kontaktanfrage',
message: error.message
});
throw error;
}
popupState.set({
type: 'info',
title: 'Kontaktanfrage abgeschickt',
message: 'Deine Kontaktanfrage wurde abgeschickt. Jemand aus dem Team wird sich nächstmöglich bei Dir melden.'
});
}
</script>

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

@ -0,0 +1,89 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import Scroll from '@components/website/index/Scroll.svelte';
import Teams from '@app/webite/index/Teams.svelte';
import Countdown from '@components/website/index/Countdown.svelte';
import Varo from '@assets/img/varo.webp';
import Background from '@assets/img/background.webp';
import { START_DATE } from 'astro:env/client';
import { getSetting, SettingKey } from '@util/settings';
import { db } from '@db/database.ts';
const teams = await db.getTeams({});
const deaths = await db.getDeaths({});
const signupEnabled = await getSetting(db, SettingKey.SignupEnabled, false);
const information = [
{
title: 'Was ist Varo?',
description:
'Varo ist ein spannendes Vanilla-Minecraft-PvP-Projekt, bei dem Zweier-Teams im Kampf ums Überleben gegeneinander antreten. Wer stirbt ganz gleich auf welche Weise scheidet endgültig aus. Das letzte verbleibende Team gewinnt Varo!'
},
{
title: 'Warum Varo?',
description:
'Varo 5 ist unser erstes großes Sommerprojekt ein PvP-Format, das für frische Abwechslung sorgt und die Wartezeit auf das nächsten CraftAttack verkürzt.\n' +
'Tatsächlich ist es schon das fünfte Varo auch wenn das viele überraschen dürfte.\n' +
'Zum ersten Mal veranstalten wir es jedoch in dieser offenen Form für alle Teilnehmenden.\n' +
'Wenn es euch gefällt, könnte Varo künftig als feste Sommerreihe weitergeführt werden.\n' +
'Und natürlich gilt: Im Winter sehen wir uns wie gewohnt bei CraftAttack wieder!'
},
{
title: 'Wer kann mitspielen?',
description:
'Alle Spieler mit einem Minecraft Java-Account sind herzlich eingeladen, mitzumachen. Wenn du dabei sein willst, brauchst du nur einen Teampartner gemeinsam könnt ihr euch direkt hier auf unserer Website anmelden.'
}
];
---
<WebsiteLayout title="Varo 5">
<div class="bg-base-200 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">
<div class="flex flex-col w-full mt-2 lg:mt-5 lg:w-10/12 h-full">
<div class="w-2/3 m-auto">
<img src={Varo.src} alt="Varo 5" />
</div>
<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>
</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>
<div class="fixed bottom-0 right-0 m-5">
<Scroll href="#teams" client:load />
</div>
<div class="bg-base-100 flex flex-col space-y-10 items-center py-10">
<h2 id="teams" class="text-4xl">Teams</h2>
<Teams {teams} {deaths} />
</div>
</WebsiteLayout>

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

@ -0,0 +1,39 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import { rulesLong } 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">Varo 5 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>{rulesLong.header}</p>
<p class="mt-1 text-[.75rem]">{rulesLong.footer}</p>
</div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
</div>
{
rulesLong.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">
<p>{section.content}</p>
</div>
</div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" />
</div>
))
}
</div>
</WebsiteLayout>

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

@ -0,0 +1,226 @@
---
import SignupLayout from '@layouts/website/SignupLayout.astro';
import Checkbox from '@components/input/Checkbox.svelte';
import Input from '@components/input/Input.svelte';
import RulesPopup from '@components/website/signup/RulesPopup.svelte';
import Popup from '@components/popup/Popup.svelte';
import TeamPopup from '@components/website/signup/TeamPopup.svelte';
import RegisteredPopup from '@components/website/signup/RegisteredPopup.svelte';
import { getSettings, SettingKey } from '@util/settings';
import { db } from '@db/database.ts';
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] ?? '';
---
<SignupLayout signupEnabled={signupEnabled}>
<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: '^\\w{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: '^\\w{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 />
<Input id="teamMember" type="text" label="Mitspieler" required dynamicWidth>
<span slot="notice"
>Trage hier den Minecraft-Spielername des Mitspieler ein, mit dem du in ein Team möchtest. Auch dieser muss
bei seiner Anmeldung deinen Namen eintragen. Nur wenn ihr beide eure Namen gegenseitig eingetragen habt, kann
ein Team erstellt werden.</span
>
</Input>
</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>
</SignupLayout>
<RulesPopup client:idle />
<Popup client:idle />
<TeamPopup client:idle />
<RegisteredPopup client:idle />
{
!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 '@components/website/signup/RulesPopup';
import { teamPopupName, teamPopupOpen } from '@components/website/signup/TeamPopup';
import { registeredPopupState } from '@components/website/signup/RegisteredPopup';
/* ----- client validation ----- */
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'));
/* send signup */
const form = document.getElementById('signup')! as HTMLFormElement;
async function sendSignup() {
const { data, 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,
teamMember: form.teamMember.value,
teamName: teamPopupName.get()
});
// must be done in order to show the team popup again if it's closed or an error occurs
teamPopupName.set(null);
if (error) {
if (error.code == 'BAD_REQUEST') {
teamPopupOpen.set(true);
const close = teamPopupName.listen(() => {
close();
sendSignup();
});
} else 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,
team: data.team.name,
teamMember: form.teamMember.value,
teamColor: data.team.color
});
const cancel = registeredPopupState.subscribe((value) => {
if (value) return;
cancel();
form.reset();
});
}
form.addEventListener('submit', (e) => {
e.preventDefault();
sendSignup();
});
</script>

76
src/pages/team.astro Normal file
View File

@ -0,0 +1,76 @@
---
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
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' }]
},
{
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' }]
},
{
name: 'Lars',
nickname: '28Pupsi28',
roles: ['Support', 'Softwareentwicklung'],
links: [
{
name: 'Website',
href: 'https://mathemann.ddns.net/turtle_game/',
icon: 'heroicons:globe-alt'
}
]
},
{
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">Das Team</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="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}`}
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>
</div>
</div>
</div>
))
}
</div>
</div>
</WebsiteLayout>