Compare commits

...

11 Commits

Author SHA1 Message Date
fc156c386b show user heads in team table
All checks were successful
deploy / build-and-deploy (push) Successful in 16s
2025-06-04 00:13:59 +02:00
a8a6424098 update teams table 2025-06-03 23:59:28 +02:00
c1dcaf05bf update signup name validation pattern 2025-06-03 23:50:09 +02:00
56e0e15a04 make astro module scripts view transition aware 2025-06-03 23:28:31 +02:00
e09a232f3c cancel subscriptions on signup popup destroy 2025-06-03 23:28:12 +02:00
54352d7b73 fix registered popup not reset state on close 2025-06-03 22:48:14 +02:00
b8112d66db fix rules popup having timeout in dev mode 2025-06-03 22:47:39 +02:00
cb0c453279 update team popup text 2025-06-03 22:47:25 +02:00
1d43eb8b90 add incomplete team notice 2025-06-03 22:47:10 +02:00
6a9f88c9f7 rename team to admins 2025-06-03 22:05:03 +02:00
dc9ef2bb84 check team member uuid on signup 2025-06-03 21:59:00 +02:00
11 changed files with 223 additions and 179 deletions

View File

@ -66,6 +66,15 @@ export const signup = {
}); });
} }
try {
await getJavaUuid(input.teamMember);
} catch (_) {
throw new ActionError({
code: 'NOT_FOUND',
message: `Der Minecraft Java Account deines Mitspieler mit dem Username ${input.username} wurde nicht gefunden`
});
}
// check if user is blocked // check if user is blocked
if (uuid) { if (uuid) {
const blockedUser = await db.getBlockedUserByUuid({ uuid: uuid }); const blockedUser = await db.getBlockedUserByUuid({ uuid: uuid });

View File

@ -4,7 +4,7 @@
import MenuRules from '@assets/img/menu-rules.webp'; import MenuRules from '@assets/img/menu-rules.webp';
import MenuFaq from '@assets/img/menu-faq.webp'; import MenuFaq from '@assets/img/menu-faq.webp';
import MenuFeedback from '@assets/img/menu-feedback.webp'; import MenuFeedback from '@assets/img/menu-feedback.webp';
import MenuTeam from '@assets/img/menu-team.webp'; import MenuAdmins from '@assets/img/menu-admins.webp';
import MenuButton from '@assets/img/menu-button.webp'; import MenuButton from '@assets/img/menu-button.webp';
import MenuInventoryBar from '@assets/img/menu-inventory-bar.webp'; import MenuInventoryBar from '@assets/img/menu-inventory-bar.webp';
import MenuSelectedFrame from '@assets/img/menu-selected-frame.webp'; import MenuSelectedFrame from '@assets/img/menu-selected-frame.webp';
@ -48,9 +48,9 @@
active: false active: false
}, },
{ {
name: 'Team', name: 'Admins',
sprite: MenuTeam.src, sprite: MenuAdmins.src,
href: 'team', href: 'admins',
active: false active: false
} }
]); ]);

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Steve from '@assets/img/steve.png'; import Skeleton from '@assets/img/steve.png';
import type { GetDeathsRes } from '@db/schema/death.ts'; import type { GetDeathsRes } from '@db/schema/death.ts';
import { type ActionReturnType, actions } from 'astro:actions'; import { type ActionReturnType, actions } from 'astro:actions';
@ -27,30 +27,29 @@
<td> <td>
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<div class="rounded-sm w-3 h-3" style="background-color: {team.color}"></div> <div class="rounded-sm w-3 h-3" style="background-color: {team.color}"></div>
<h3 class="text-xs sm:text-xl">{team.name}</h3> <h3 class="text-xs sm:text-xl" class:text-red-200={!team.memberOne.id || !team.memberTwo.id}>
{team.name}
</h3>
</div> </div>
{#if !team.memberOne.id || !team.memberTwo.id}
<span>Team unvollständig</span>
{/if}
</td> </td>
<td class="max-w-9 overflow-ellipsis"> <td class="max-w-9 overflow-ellipsis">
{#if team.memberOne.id} {#if team.memberOne.id}
{@const dead = deaths.findIndex((d) => d.deadUserId === team.memberOne.id)}
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<img class="w-4 h-4 pixelated" src={Steve.src} alt="head" /> <img class="w-4 h-4 pixelated" src={dead === -1 ? `https://mc-heads.net/head/${team.memberOne.username}/8` : Skeleton.src} alt="head" />
<span <span class="text-xs sm:text-md" class:line-through={dead !== -1}>{team.memberOne.username}</span>
class="text-xs sm:text-md"
class:line-through={deaths.find((d) => d.deadUserId === team.memberOne.id)}
>{team.memberOne.username}</span
>
</div> </div>
{/if} {/if}
</td> </td>
<td> <td>
{#if team.memberTwo.id} {#if team.memberTwo.id}
{@const dead = deaths.findIndex((d) => d.deadUserId === team.memberTwo.id)}
<div class="flex items-center gap-x-2"> <div class="flex items-center gap-x-2">
<img class="w-4 h-4 pixelated" src={Steve.src} alt="head" /> <img class="w-4 h-4 pixelated" src={dead === -1 ? `https://mc-heads.net/head/${team.memberTwo.username}/8` : Skeleton.src} alt="head" />
<span <span class="text-xs sm:text-md" class:line-through={dead !== -1}>{team.memberTwo.username}</span>
class="text-xs sm:text-md"
class:line-through={deaths.find((d) => d.deadUserId === team.memberTwo.id)}
>{team.memberTwo.username}</span
>
</div> </div>
{/if} {/if}
</td> </td>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { registeredPopupState } from '@app/website/signup/RegisteredPopup.ts'; import { registeredPopupState } from '@app/website/signup/RegisteredPopup.ts';
import Input from '@components/input/Input.svelte'; import Input from '@components/input/Input.svelte';
import { onDestroy } from 'svelte';
interface Props { interface Props {
discordLink: string; discordLink: string;
@ -15,7 +16,7 @@
let modal: HTMLDialogElement; let modal: HTMLDialogElement;
registeredPopupState.subscribe(async (value) => { const cancel = registeredPopupState.subscribe(async (value) => {
if (!value) return; if (!value) return;
modal.show(); modal.show();
@ -40,9 +41,11 @@
skinViewer.dispose(); skinViewer.dispose();
}); });
onDestroy(cancel);
</script> </script>
<dialog class="modal" bind:this={modal} onclose={($registeredPopupState = null)}> <dialog class="modal" bind:this={modal} onclose={() => ($registeredPopupState = null)}>
<form method="dialog" class="modal-box xl:w-5/12 max-w-10/12 z-10"> <form method="dialog" class="modal-box xl:w-5/12 max-w-10/12 z-10">
<h1 class="text-center text-xl sm:text-3xl mb-8">Registrierung erfolgreich</h1> <h1 class="text-center text-xl sm:text-3xl mb-8">Registrierung erfolgreich</h1>
<p class="text-center font-bold"> <p class="text-center font-bold">

View File

@ -2,15 +2,16 @@
import { rulesPopupState, rulesPopupRead } from './RulesPopup.ts'; import { rulesPopupState, rulesPopupRead } from './RulesPopup.ts';
import { rules } from '../../../rules.ts'; import { rules } from '../../../rules.ts';
import { popupState } from '@components/popup/Popup.ts'; import { popupState } from '@components/popup/Popup.ts';
import { onDestroy } from 'svelte';
const modalTimeoutSeconds = 30; const modalTimeoutSeconds = 30;
let modalElem: HTMLDialogElement; let modalElem: HTMLDialogElement;
let modalTimer = $state<ReturnType<typeof setInterval> | null>(null); let modalTimer = $state<ReturnType<typeof setInterval> | null>(null);
let modalSecondsOpen = $state(0); let modalSecondsOpen = $state(import.meta.env.PROD ? 0 : modalTimeoutSeconds);
rulesPopupState.listen((value) => { const cancel = rulesPopupState.subscribe((value) => {
if (value == 'open') { if (value == 'open') {
modalElem.show(); modalElem.show();
modalTimer = setInterval(() => modalSecondsOpen++, 1000); modalTimer = setInterval(() => modalSecondsOpen++, 1000);
@ -18,10 +19,11 @@
clearInterval(modalTimer!); clearInterval(modalTimer!);
} }
}); });
onDestroy(cancel);
</script> </script>
<dialog <dialog
id="rules-popup"
class="modal" class="modal"
onclose={() => { onclose={() => {
if ($rulesPopupState !== 'accepted') $rulesPopupState = 'closed'; if ($rulesPopupState !== 'accepted') $rulesPopupState = 'closed';

View File

@ -1,13 +1,17 @@
<script lang="ts"> <script lang="ts">
import { teamPopupOpen, teamPopupName } from '@app/website/signup/TeamPopup.ts'; import { teamPopupOpen, teamPopupName } from '@app/website/signup/TeamPopup.ts';
import Input from '@components/input/Input.svelte';
import { onDestroy } from 'svelte';
let modal: HTMLDialogElement; let modal: HTMLDialogElement;
let form: HTMLFormElement; let form: HTMLFormElement;
teamPopupOpen.subscribe((value) => { const cancel = teamPopupOpen.subscribe((value) => {
if (value) modal.show(); if (value) modal.show();
else form?.reset(); else form?.reset();
}); });
onDestroy(cancel);
</script> </script>
<dialog class="modal" bind:this={modal} onclose={() => ($teamPopupOpen = false)}> <dialog class="modal" bind:this={modal} onclose={() => ($teamPopupOpen = false)}>
@ -17,13 +21,17 @@
</form> </form>
<form method="dialog" bind:this={form} onsubmit={() => ($teamPopupName = form.teamName.value)}> <form method="dialog" bind:this={form} onsubmit={() => ($teamPopupName = form.teamName.value)}>
<h3 class="text-lg font-geist">Team erstellen</h3> <h3 class="text-lg font-geist">Team erstellen</h3>
<p class="py-4">Es wurde noch kein Team für dich und deinen Mitspieler erstellt.</p> <p class="py-4">Es wurde noch kein Team für dich und deinen Mitspieler erstellt. Wie soll euer Team heißen?</p>
<fieldset class="fieldset"> <Input id="teamName" type="text" label="Teamname" required>
<legend class="fieldset-legend"> {#snippet notice()}
<span>Teamname <span class="text-red-700">*</span></span> <span>
</legend> Dein Team ist erst vollständig registriert, wenn dein Teamparter sich ebenfalls angemeldet hat. Eine
<input id="teamName" name="teamName" class="input validator" type="text" required /> Teilnahme ohne Teampartner ist nicht möglich.
</fieldset> <br />
Prüfe bitte nach, ob dein Team auf der Startseite aufgelistet wird, sobald beide Teammitglieder registriert sind!
</span>
{/snippet}
</Input>
<button class="mt-4 btn btn-neutral">Team registrieren</button> <button class="mt-4 btn btn-neutral">Team registrieren</button>
</form> </form>
</div> </div>

View File

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 156 B

View File

@ -30,7 +30,7 @@ const team = [
<WebsiteLayout title="Team"> <WebsiteLayout title="Team">
<div class="m-auto flex flex-col justify-center items-center px-4 py-6 2xl:px-48 sm:py-12"> <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> <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"> <div class="grid md:grid-cols-2 xl:grid-cols-3 gap-4 my-4 justify-center">
{ {
team.map((member) => ( team.map((member) => (

View File

@ -32,82 +32,91 @@ import Input from '@components/input/Input.svelte';
import { popupState } from '@components/popup/Popup'; import { popupState } from '@components/popup/Popup';
import { actionErrorPopup } from '../util/action'; import { actionErrorPopup } from '../util/action';
const form = document.getElementById('feedback-contact') as HTMLFormElement; function setupForm() {
const type = document.getElementById('type') as HTMLSelectElement; const form = document.getElementById('feedback-contact') as HTMLFormElement;
const content = document.getElementById('content') as HTMLTextAreaElement; const type = document.getElementById('type') as HTMLSelectElement;
const email = document.getElementById('email') as HTMLInputElement; const content = document.getElementById('content') as HTMLTextAreaElement;
const email = document.getElementById('email') as HTMLInputElement;
// reset form on site (re-)load // reset form on site (re-)load
form.reset(); form.reset();
type.addEventListener('change', () => { type.addEventListener('change', () => {
if (type.value === 'website-feedback') { if (type.value === 'website-feedback') {
// content input // content input
content.previousElementSibling!.firstChild!.textContent = 'Feedback'; content.previousElementSibling!.firstChild!.textContent = 'Feedback';
// email input // email input
email.parentElement!.hidden = true; email.parentElement!.hidden = true;
email.required = false; email.required = false;
} else if (type.value === 'website-contact') { } else if (type.value === 'website-contact') {
// content input // content input
content.previousElementSibling!.firstChild!.textContent = 'Anfrage'; content.previousElementSibling!.firstChild!.textContent = 'Anfrage';
// email input // email input
email.required = true; email.required = true;
email.parentElement!.hidden = false; 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({
content: content.value
}); });
if (error) { email.required = false;
actionErrorPopup(error);
return;
}
popupState.set({ form.addEventListener('submit', async (e) => {
type: 'info', e.preventDefault();
title: 'Feedback abgeschickt',
message: 'Dein Feedback wurde abgeschickt. Vielen Dank, dass du uns hilfst, das Projekt besser zu machen!' 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 Team wird sich nächstmöglich bei Dir melden.'
});
};
} }
async function sendContact() { const pathname = document.location.pathname;
const { error } = await actions.feedback.addWebsiteContact({ document.addEventListener('astro:page-load', () => {
content: content.value, if (document.location.pathname !== pathname) return;
email: email.value
});
if (error) { setupForm();
actionErrorPopup(error); });
return;
}
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> </script>

View File

@ -83,7 +83,11 @@ const information = [
</div> </div>
<div class="bg-base-100 flex flex-col space-y-10 items-center py-10"> <div class="bg-base-100 flex flex-col space-y-10 items-center py-10">
<h2 id="teams" class="text-4xl">Teams</h2> <h2 id="teams" class="text-4xl mb-10">Teams</h2>
<p class="text-sm text-center mb-2 mx-1">
Bei Unvollständigen Teams muss sich der zweite Mitspieler noch registrieren. Unvollständige Teams werden bei
Anmeldeschluss gelöscht.
</p>
<Teams {teams} {deaths} /> <Teams {teams} {deaths} />
</div> </div>
</WebsiteLayout> </WebsiteLayout>

View File

@ -36,7 +36,7 @@ const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessa
label="Vorname" label="Vorname"
required required
validation={{ validation={{
pattern: '^\\w{2,}', pattern: '^\\p{L}{2,}',
hint: 'Bitte gib Deinen vollständigen Vornamen an, dieser muss mindestens aus 2 Zeichen bestehen.' hint: 'Bitte gib Deinen vollständigen Vornamen an, dieser muss mindestens aus 2 Zeichen bestehen.'
}} }}
dynamicWidth dynamicWidth
@ -47,7 +47,7 @@ const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessa
label="Nachname" label="Nachname"
required required
validation={{ validation={{
pattern: '^\\w{2,}', pattern: '^\\p{L}{2,}',
hint: 'Bitte gib Deinen vollständigen Nachnamen an, dieser muss mindestens aus 2 Zeichen bestehen.' hint: 'Bitte gib Deinen vollständigen Nachnamen an, dieser muss mindestens aus 2 Zeichen bestehen.'
}} }}
dynamicWidth dynamicWidth
@ -155,89 +155,99 @@ const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessa
import { teamPopupName, teamPopupOpen } from '@app/website/signup/TeamPopup'; import { teamPopupName, teamPopupOpen } from '@app/website/signup/TeamPopup';
import { registeredPopupState } from '@app/website/signup/RegisteredPopup'; import { registeredPopupState } from '@app/website/signup/RegisteredPopup';
/* ----- client validation ----- */ function setupClientValidation() {
const rulesCheckbox = document.getElementById('rules')! as HTMLInputElement; const rulesCheckbox = document.getElementById('rules') as HTMLInputElement;
const rulesCheckboxRulesLink = rulesCheckbox.nextElementSibling!.querySelector('.link') as HTMLAnchorElement; const rulesCheckboxRulesLink = rulesCheckbox.nextElementSibling!.querySelector('.link') as HTMLAnchorElement;
// add popup state subscriber to check when the accepted button is clicked // add popup state subscriber to check when the accepted button is clicked
rulesPopupState.subscribe((value) => { rulesPopupState.subscribe((value) => {
if (value == 'accepted') rulesCheckbox.checked = true; 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'));
/* ----- signup form ----- */
const form = document.getElementById('signup')! as HTMLFormElement;
// reset form on site (re-)load
form.reset();
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 // add click handler to open rules popup to rules checkbox
teamPopupName.set(null); rulesCheckbox.addEventListener('click', (e) => {
if (!rulesPopupRead.get()) {
if (error) { e.preventDefault();
if (error.code == 'BAD_REQUEST') { rulesPopupState.set('open');
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) => { // add click handler to open rules popup when clicking the rules link in the rules checkbox label
if (value) return; rulesCheckboxRulesLink!.addEventListener('click', () => rulesPopupState.set('open'));
cancel(); }
form.reset();
function setupForm() {
const form = document.getElementById('signup')! as HTMLFormElement;
// reset form on site (re-)load
form.reset();
const sendSignup = async () => {
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();
}); });
} }
form.addEventListener('submit', (e) => { const pathname = document.location.pathname;
e.preventDefault(); document.addEventListener('astro:page-load', () => {
sendSignup(); if (document.location.pathname !== pathname) return;
setupClientValidation();
setupForm();
}); });
</script> </script>