add direct invitation link
All checks were successful
deploy / build-and-deploy (push) Successful in 21s
All checks were successful
deploy / build-and-deploy (push) Successful in 21s
This commit is contained in:
28
src/app/admin/directInvitations/DirectInvitations.svelte
Normal file
28
src/app/admin/directInvitations/DirectInvitations.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import DataTable from '@components/admin/table/DataTable.svelte';
|
||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||
import {
|
||||
deleteDirectInvitation,
|
||||
type DirectInvitation,
|
||||
directInvitations
|
||||
} from '@app/admin/directInvitations/directInvitations.ts';
|
||||
|
||||
// callback
|
||||
function onDirectInvitationDelete(directInvitation: DirectInvitation) {
|
||||
$confirmPopupState = {
|
||||
title: 'Direkte Einladung löschen?',
|
||||
message: 'Soll die direkte Einladung wirklich entblockiert werden?',
|
||||
onConfirm: () => deleteDirectInvitation(directInvitation)
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<DataTable
|
||||
data={directInvitations}
|
||||
count={true}
|
||||
keys={[
|
||||
{ key: 'url', label: 'Link', width: 50 },
|
||||
{ key: 'user.username', label: 'Registrierter Nutzer', width: 40, sortable: true }
|
||||
]}
|
||||
onDelete={onDirectInvitationDelete}
|
||||
/>
|
||||
32
src/app/admin/directInvitations/SidebarActions.svelte
Normal file
32
src/app/admin/directInvitations/SidebarActions.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import Icon from '@iconify/svelte';
|
||||
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
|
||||
import { addDirectInvitation, fetchDirectInvitations } from '@app/admin/directInvitations/directInvitations.ts';
|
||||
|
||||
// states
|
||||
let createPopupOpen = $state(false);
|
||||
|
||||
// lifecycle
|
||||
$effect(() => {
|
||||
fetchDirectInvitations();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
|
||||
<Icon icon="heroicons:plus-16-solid" />
|
||||
<span>Neue direkte Einladung</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CrudPopup
|
||||
texts={{
|
||||
title: 'Direkte Einladung erstellen',
|
||||
submitButtonTitle: 'Erstellen',
|
||||
confirmPopupTitle: 'Direkte Einladung erstellen',
|
||||
confirmPopupMessage: 'Bist du sicher, dass die direkte Einladung erstellt werden soll?.'
|
||||
}}
|
||||
target={null}
|
||||
onSubmit={addDirectInvitation}
|
||||
bind:open={createPopupOpen}
|
||||
/>
|
||||
45
src/app/admin/directInvitations/directInvitations.ts
Normal file
45
src/app/admin/directInvitations/directInvitations.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { type ActionReturnType, actions } from 'astro:actions';
|
||||
import { writable } from 'svelte/store';
|
||||
import { actionErrorPopup } from '@util/action.ts';
|
||||
import { addToWritableArray, deleteFromWritableArray } from '@util/state.ts';
|
||||
|
||||
// types
|
||||
export type DirectInvitations = Exclude<
|
||||
ActionReturnType<typeof actions.user.directInvitations>['data'],
|
||||
undefined
|
||||
>['directInvitations'];
|
||||
export type DirectInvitation = DirectInvitations[0];
|
||||
|
||||
// state
|
||||
export const directInvitations = writable<DirectInvitations>([]);
|
||||
|
||||
// actions
|
||||
export async function fetchDirectInvitations() {
|
||||
const { data, error } = await actions.user.directInvitations();
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
directInvitations.set(data.directInvitations);
|
||||
}
|
||||
|
||||
export async function addDirectInvitation() {
|
||||
const { data, error } = await actions.user.addDirectInvitation();
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
addToWritableArray(directInvitations, data);
|
||||
}
|
||||
|
||||
export async function deleteDirectInvitation(directInvitation: DirectInvitation) {
|
||||
const { error } = await actions.user.deleteDirectInvitation(directInvitation);
|
||||
if (error) {
|
||||
actionErrorPopup(error);
|
||||
return;
|
||||
}
|
||||
|
||||
deleteFromWritableArray(directInvitations, (i) => i.hash === directInvitation.hash);
|
||||
}
|
||||
@@ -8,9 +8,10 @@
|
||||
paypalLink: string;
|
||||
teamspeakLink: string;
|
||||
startDate: string;
|
||||
newRegistrationAllowed: boolean;
|
||||
}
|
||||
|
||||
let { discordLink, paypalLink, teamspeakLink, startDate }: Props = $props();
|
||||
let { discordLink, paypalLink, teamspeakLink, startDate, newRegistrationAllowed }: Props = $props();
|
||||
|
||||
let skin: string | null = $state(null);
|
||||
|
||||
@@ -87,10 +88,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="flex justify-center gap-8">
|
||||
<button class="btn">Weitere Person anmelden</button>
|
||||
</div>
|
||||
{#if newRegistrationAllowed}
|
||||
<div class="divider"></div>
|
||||
<div class="flex justify-center gap-8">
|
||||
<button class="btn">Weitere Person anmelden</button>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
<div class="absolute w-full h-full bg-black/50"></div>
|
||||
</dialog>
|
||||
|
||||
245
src/app/website/signup/Signup.astro
Normal file
245
src/app/website/signup/Signup.astro
Normal file
@@ -0,0 +1,245 @@
|
||||
---
|
||||
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 { DISCORD_LINK, PAYPAL_LINK, START_DATE, TEAMSPEAK_LINK } from 'astro:env/server';
|
||||
|
||||
interface Props {
|
||||
signupDisabled?: {
|
||||
message: string;
|
||||
subMessage?: string;
|
||||
};
|
||||
signupHash?: string;
|
||||
}
|
||||
|
||||
const { signupDisabled, signupHash } = Astro.props;
|
||||
---
|
||||
|
||||
<WebsiteLayout title="Anmeldung" footer={false}>
|
||||
<div
|
||||
class="flex justify-center w-full min-h-screen bg-base-200"
|
||||
class:list={[signupDisabled ? '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" data-signup-hash={signupHash}>
|
||||
<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>
|
||||
<span slot="notice" class="text-[.65rem]"
|
||||
>Spielern der Bedrock-Edition wird empfohlen, ihren Microsoft-Account Namen einzugeben. Der
|
||||
Bedrock-Spielername kann nicht immer einem Minecraft Account zugeordnet werden.</span
|
||||
>
|
||||
</Input>
|
||||
<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}
|
||||
newRegistrationAllowed={!signupHash}
|
||||
/>
|
||||
|
||||
{
|
||||
signupDisabled && (
|
||||
<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>{signupDisabled.message}</h1>
|
||||
<h3>{signupDisabled.subMessage}</h3>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<script>
|
||||
import { actions } from 'astro:actions';
|
||||
import { popupState } from '@components/popup/Popup.ts';
|
||||
import { rulesPopupState, rulesPopupRead } from '@app/website/signup/RulesPopup.ts';
|
||||
import { registeredPopupState } from '@app/website/signup/RegisteredPopup.ts';
|
||||
|
||||
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,
|
||||
hash: form.dataset.signupHash
|
||||
});
|
||||
|
||||
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