rewrite website
This commit is contained in:
14
src/pages/404.astro
Normal file
14
src/pages/404.astro
Normal 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
12
src/pages/500.astro
Normal 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>
|
||||
16
src/pages/admin/admins/index.astro
Normal file
16
src/pages/admin/admins/index.astro
Normal 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>
|
||||
16
src/pages/admin/admins/strike_reasons.astro
Normal file
16
src/pages/admin/admins/strike_reasons.astro
Normal 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>
|
||||
14
src/pages/admin/feedback.astro
Normal file
14
src/pages/admin/feedback.astro
Normal 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>
|
||||
10
src/pages/admin/index.astro
Normal file
10
src/pages/admin/index.astro
Normal 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" />
|
||||
49
src/pages/admin/login.astro
Normal file
49
src/pages/admin/login.astro
Normal 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>
|
||||
27
src/pages/admin/reports/attachment/[fileHash].ts
Normal file
27
src/pages/admin/reports/attachment/[fileHash].ts
Normal 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()
|
||||
}
|
||||
});
|
||||
};
|
||||
16
src/pages/admin/reports/index.astro
Normal file
16
src/pages/admin/reports/index.astro
Normal 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>
|
||||
17
src/pages/admin/settings.astro
Normal file
17
src/pages/admin/settings.astro
Normal 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>
|
||||
14
src/pages/admin/tools/index.astro
Normal file
14
src/pages/admin/tools/index.astro
Normal 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>
|
||||
16
src/pages/admin/users/blocked.astro
Normal file
16
src/pages/admin/users/blocked.astro
Normal 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>
|
||||
16
src/pages/admin/users/index.astro
Normal file
16
src/pages/admin/users/index.astro
Normal 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
89
src/pages/admins.astro
Normal 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
37
src/pages/api/feedback.ts
Normal 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
33
src/pages/api/player.ts
Normal 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
103
src/pages/api/report.ts
Normal 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
220
src/pages/faq.astro
Normal 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
123
src/pages/feedback.astro
Normal 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>
|
||||
66
src/pages/feedback/[urlHash].astro
Normal file
66
src/pages/feedback/[urlHash].astro
Normal 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
114
src/pages/index.astro
Normal 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>
|
||||
23
src/pages/report/[urlHash].astro
Normal file
23
src/pages/report/[urlHash].astro
Normal 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 />
|
||||
106
src/pages/report/_draft.astro
Normal file
106
src/pages/report/_draft.astro
Normal 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>
|
||||
33
src/pages/report/_submitted.astro
Normal file
33
src/pages/report/_submitted.astro
Normal 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
37
src/pages/rules.astro
Normal 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
239
src/pages/signup.astro
Normal 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>
|
||||
Reference in New Issue
Block a user