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
if (uuid) {
const blockedUser = await db.getBlockedUserByUuid({ uuid: uuid });

View File

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

View File

@ -1,5 +1,5 @@
<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 ActionReturnType, actions } from 'astro:actions';
@ -27,30 +27,29 @@
<td>
<div class="flex items-center gap-x-2">
<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>
{#if !team.memberOne.id || !team.memberTwo.id}
<span>Team unvollständig</span>
{/if}
</td>
<td class="max-w-9 overflow-ellipsis">
{#if team.memberOne.id}
{@const dead = deaths.findIndex((d) => d.deadUserId === team.memberOne.id)}
<div class="flex items-center gap-x-2">
<img class="w-4 h-4 pixelated" src={Steve.src} alt="head" />
<span
class="text-xs sm:text-md"
class:line-through={deaths.find((d) => d.deadUserId === team.memberOne.id)}
>{team.memberOne.username}</span
>
<img class="w-4 h-4 pixelated" src={dead === -1 ? `https://mc-heads.net/head/${team.memberOne.username}/8` : Skeleton.src} alt="head" />
<span class="text-xs sm:text-md" class:line-through={dead !== -1}>{team.memberOne.username}</span>
</div>
{/if}
</td>
<td>
{#if team.memberTwo.id}
{@const dead = deaths.findIndex((d) => d.deadUserId === team.memberTwo.id)}
<div class="flex items-center gap-x-2">
<img class="w-4 h-4 pixelated" src={Steve.src} alt="head" />
<span
class="text-xs sm:text-md"
class:line-through={deaths.find((d) => d.deadUserId === team.memberTwo.id)}
>{team.memberTwo.username}</span
>
<img class="w-4 h-4 pixelated" src={dead === -1 ? `https://mc-heads.net/head/${team.memberTwo.username}/8` : Skeleton.src} alt="head" />
<span class="text-xs sm:text-md" class:line-through={dead !== -1}>{team.memberTwo.username}</span>
</div>
{/if}
</td>

View File

@ -1,6 +1,7 @@
<script lang="ts">
import { registeredPopupState } from '@app/website/signup/RegisteredPopup.ts';
import Input from '@components/input/Input.svelte';
import { onDestroy } from 'svelte';
interface Props {
discordLink: string;
@ -15,7 +16,7 @@
let modal: HTMLDialogElement;
registeredPopupState.subscribe(async (value) => {
const cancel = registeredPopupState.subscribe(async (value) => {
if (!value) return;
modal.show();
@ -40,9 +41,11 @@
skinViewer.dispose();
});
onDestroy(cancel);
</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">
<h1 class="text-center text-xl sm:text-3xl mb-8">Registrierung erfolgreich</h1>
<p class="text-center font-bold">

View File

@ -2,15 +2,16 @@
import { rulesPopupState, rulesPopupRead } from './RulesPopup.ts';
import { rules } from '../../../rules.ts';
import { popupState } from '@components/popup/Popup.ts';
import { onDestroy } from 'svelte';
const modalTimeoutSeconds = 30;
let modalElem: HTMLDialogElement;
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') {
modalElem.show();
modalTimer = setInterval(() => modalSecondsOpen++, 1000);
@ -18,10 +19,11 @@
clearInterval(modalTimer!);
}
});
onDestroy(cancel);
</script>
<dialog
id="rules-popup"
class="modal"
onclose={() => {
if ($rulesPopupState !== 'accepted') $rulesPopupState = 'closed';

View File

@ -1,13 +1,17 @@
<script lang="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 form: HTMLFormElement;
teamPopupOpen.subscribe((value) => {
const cancel = teamPopupOpen.subscribe((value) => {
if (value) modal.show();
else form?.reset();
});
onDestroy(cancel);
</script>
<dialog class="modal" bind:this={modal} onclose={() => ($teamPopupOpen = false)}>
@ -17,13 +21,17 @@
</form>
<form method="dialog" bind:this={form} onsubmit={() => ($teamPopupName = form.teamName.value)}>
<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>
<fieldset class="fieldset">
<legend class="fieldset-legend">
<span>Teamname <span class="text-red-700">*</span></span>
</legend>
<input id="teamName" name="teamName" class="input validator" type="text" required />
</fieldset>
<p class="py-4">Es wurde noch kein Team für dich und deinen Mitspieler erstellt. Wie soll euer Team heißen?</p>
<Input id="teamName" type="text" label="Teamname" required>
{#snippet notice()}
<span>
Dein Team ist erst vollständig registriert, wenn dein Teamparter sich ebenfalls angemeldet hat. Eine
Teilnahme ohne Teampartner ist nicht möglich.
<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>
</form>
</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">
<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">
{
team.map((member) => (

View File

@ -32,82 +32,91 @@ import Input from '@components/input/Input.svelte';
import { popupState } from '@components/popup/Popup';
import { actionErrorPopup } from '../util/action';
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;
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();
// 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())
});
}
});
async function sendFeedback() {
const { error } = await actions.feedback.addWebsiteFeedback({
content: content.value
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;
}
});
if (error) {
actionErrorPopup(error);
return;
}
email.required = false;
popupState.set({
type: 'info',
title: 'Feedback abgeschickt',
message: 'Dein Feedback wurde abgeschickt. Vielen Dank, dass du uns hilfst, das Projekt besser zu machen!'
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 Team wird sich nächstmöglich bei Dir melden.'
});
};
}
async function sendContact() {
const { error } = await actions.feedback.addWebsiteContact({
content: content.value,
email: email.value
});
const pathname = document.location.pathname;
document.addEventListener('astro:page-load', () => {
if (document.location.pathname !== pathname) return;
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.'
});
}
setupForm();
});
</script>

View File

@ -83,7 +83,11 @@ const information = [
</div>
<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} />
</div>
</WebsiteLayout>

View File

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