move components
This commit is contained in:
69
src/app/website/index/Countdown.svelte
Normal file
69
src/app/website/index/Countdown.svelte
Normal file
@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
let { start, end }: { start?: number; end: number } = $props();
|
||||
|
||||
let title = `Spielstart ist am ${new Date(import.meta.env.PUBLIC_START_DATE).toLocaleString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})} Uhr`;
|
||||
|
||||
function getUntil(): [number, number, number, number] {
|
||||
let diff = (end - (start || Date.now())) / 1000;
|
||||
|
||||
return [
|
||||
Math.floor(diff / (60 * 60 * 24)),
|
||||
Math.floor((diff % (60 * 60 * 24)) / (60 * 60)),
|
||||
Math.floor((diff % (60 * 60)) / 60),
|
||||
Math.floor(diff % 60)
|
||||
];
|
||||
}
|
||||
|
||||
let [days, hours, minutes, seconds] = $state(getUntil());
|
||||
let intervalId = setInterval(() => {
|
||||
[days, hours, minutes, seconds] = getUntil();
|
||||
if (start) start += 1000;
|
||||
}, 1000);
|
||||
|
||||
onDestroy(() => clearInterval(intervalId));
|
||||
</script>
|
||||
|
||||
<div
|
||||
class:hidden={days + hours + minutes + seconds < 0}
|
||||
class="grid grid-flow-col gap-5 text-center auto-cols-max text-white"
|
||||
>
|
||||
<div class="flex flex-col p-2 bg-gray-200/5 rounded-box backdrop-blur-sm" {title}>
|
||||
<span class="countdown font-mono text-3xl sm:text-6xl">
|
||||
<span class="m-auto" style="--value:{days};"></span>
|
||||
</span>
|
||||
Tage
|
||||
</div>
|
||||
<div class="flex flex-col p-2 bg-gray-200/5 rounded-box backdrop-blur-sm" {title}>
|
||||
<span class="countdown font-mono text-3xl sm:text-6xl">
|
||||
<span class="m-auto" style="--value:{hours};"></span>
|
||||
</span>
|
||||
Stunden
|
||||
</div>
|
||||
<div class="flex flex-col p-2 bg-gray-200/5 rounded-box backdrop-blur-sm" {title}>
|
||||
<span class="countdown font-mono text-3xl sm:text-6xl">
|
||||
<span class="m-auto" style="--value:{minutes};"></span>
|
||||
</span>
|
||||
Minuten
|
||||
</div>
|
||||
<div class="flex flex-col p-2 bg-gray-200/5 rounded-box backdrop-blur-sm" {title}>
|
||||
<span class="countdown font-mono text-3xl sm:text-6xl">
|
||||
<span class="m-auto" style="--value:{seconds};"></span>
|
||||
</span>
|
||||
Sekunden
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Set a custom content for the countdown before selector as it only supports numbers up to 99 */
|
||||
.countdown > ::before {
|
||||
content: '00\A 01\A 02\A 03\A 04\A 05\A 06\A 07\A 08\A 09\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A 18\A 19\A 20\A 21\A 22\A 23\A 24\A 25\A 26\A 27\A 28\A 29\A 30\A 31\A 32\A 33\A 34\A 35\A 36\A 37\A 38\A 39\A 40\A 41\A 42\A 43\A 44\A 45\A 46\A 47\A 48\A 49\A 50\A 51\A 52\A 53\A 54\A 55\A 56\A 57\A 58\A 59\A 60\A 61\A 62\A 63\A 64\A 65\A 66\A 67\A 68\A 69\A 70\A 71\A 72\A 73\A 74\A 75\A 76\A 77\A 78\A 79\A 80\A 81\A 82\A 83\A 84\A 85\A 86\A 87\A 88\A 89\A 90\A 91\A 92\A 93\A 94\A 95\A 96\A 97\A 98\A 99\A 100\A 101\A 102\A 103\A 104\A 105\A 106\A 107\A 108\A 109\A 110\A 111\A 112\A 113\A 114\A 115\A 116\A 117\A 118\A 119\A 120\A 121\A 122\A 123\A 124\A 125\A 126\A 127\A 128\A 129\A 130\A 131\A 132\A 133\A 134\A 135\A 136\A 137\A 138\A 139\A 140\A 141\A 142\A 143\A 144\A 145\A 146\A 147\A 148\A 149\A 150\A 151\A 152\A 153\A 154\A 155\A 156\A 157\A 158\A 159\A 160\A 161\A 162\A 163\A 164\A 165\A 166\A 167\A 168\A 169\A 170\A 171\A 172\A 173\A 174\A 175\A 176\A 177\A 178\A 179\A 180\A 181\A 182\A 183\A 184\A 185\A 186\A 187\A 188\A 189\A 190\A 191\A 192\A 193\A 194\A 195\A 196\A 197\A 198\A 199\A 200\A 201\A 202\A 203\A 204\A 205\A 206\A 207\A 208\A 209\A 210\A 211\A 212\A 213\A 214\A 215\A 216\A 217\A 218\A 219\A 220\A 221\A 222\A 223\A 224\A 225\A 226\A 227\A 228\A 229\A 230\A 231\A 232\A 233\A 234\A 235\A 236\A 237\A 238\A 239\A 240\A 241\A 242\A 243\A 244\A 245\A 246\A 247\A 248\A 249\A 250\A 251\A 252\A 253\A 254\A 255\A 256\A 257\A 258\A 259\A 260\A 261\A 262\A 263\A 264\A 265\A 266\A 267\A 268\A 269\A 270\A 271\A 272\A 273\A 274\A 275\A 276\A 277\A 278\A 279\A 280\A 281\A 282\A 283\A 284\A 285\A 286\A 287\A 288\A 289\A 290\A 291\A 292\A 293\A 294\A 295\A 296\A 297\A 298\A 299\A 300\A 301\A 302\A 303\A 304\A 305\A 306\A 307\A 308\A 309\A 310\A 311\A 312\A 313\A 314\A 315\A 316\A 317\A 318\A 319\A 320\A 321\A 322\A 323\A 324\A 325\A 326\A 327\A 328\A 329\A 330\A 331\A 332\A 333\A 334\A 335\A 336\A 337\A 338\A 339\A 340\A 341\A 342\A 343\A 344\A 345\A 346\A 347\A 348\A 349\A 350\A 351\A 352\A 353\A 354\A 355\A 356\A 357\A 358\A 359\A 360\A 361\A 362\A 363\A 364\A';
|
||||
}
|
||||
</style>
|
39
src/app/website/index/Scroll.svelte
Normal file
39
src/app/website/index/Scroll.svelte
Normal file
@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
const { href } = $props();
|
||||
|
||||
let scrollY = $state(0);
|
||||
</script>
|
||||
|
||||
<svelte:window bind:scrollY />
|
||||
|
||||
<div class="flex items-center gap-x-2 transition-opacity duration-250" class:opacity-0={scrollY > 0}>
|
||||
<div class="divider divider-horizontal m-0"></div>
|
||||
<a {href} aria-label="scroll to teams">
|
||||
<div class="border-accent border-2 rounded-t-full rounded-b-full h-7 w-4 p-1">
|
||||
<div class="bg-accent rounded-full h-1 w-1 bounce"></div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="link text-sm" {href}>Zu den Teams</a>
|
||||
<div class="divider divider-horizontal m-0"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes scrollDown {
|
||||
0% {
|
||||
transform: none;
|
||||
opacity: 0.25;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0%);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(250%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bounce {
|
||||
animation: scrollDown 2s infinite;
|
||||
}
|
||||
</style>
|
64
src/app/website/index/Teams.svelte
Normal file
64
src/app/website/index/Teams.svelte
Normal file
@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import Steve from '@assets/img/steve.png';
|
||||
import type { GetDeathsRes } from '@db/schema/death.ts';
|
||||
import { type ActionReturnType, actions } from 'astro:actions';
|
||||
|
||||
interface Props {
|
||||
teams: Exclude<ActionReturnType<typeof actions.team.teams>['data'], undefined>['teams'];
|
||||
deaths: GetDeathsRes;
|
||||
}
|
||||
|
||||
const { teams, deaths }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-300 shadow-sm w-full md:w-5/7 xl:w-4/7 sm:p-5 md:p-10">
|
||||
<table class="table table-fixed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team</th>
|
||||
<th>Spieler 1</th>
|
||||
<th>Spieler 2</th>
|
||||
<th>Kills</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each teams as team (team.id)}
|
||||
<tr>
|
||||
<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>
|
||||
</div>
|
||||
</td>
|
||||
<td class="max-w-9 overflow-ellipsis">
|
||||
{#if 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
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{#if 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
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-xs sm:text-md">0</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
102
src/app/website/signup/RegisteredPopup.svelte
Normal file
102
src/app/website/signup/RegisteredPopup.svelte
Normal file
@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { registeredPopupState } from '@app/website/signup/RegisteredPopup.ts';
|
||||
import Input from '@components/input/Input.svelte';
|
||||
|
||||
interface Props {
|
||||
discordLink: string;
|
||||
paypalLink: string;
|
||||
teamspeakLink: string;
|
||||
startDate: string;
|
||||
}
|
||||
|
||||
let { discordLink, paypalLink, teamspeakLink, startDate }: Props = $props();
|
||||
|
||||
let skin: string | null = $state(null);
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
registeredPopupState.subscribe(async (value) => {
|
||||
if (!value) return;
|
||||
|
||||
modal.show();
|
||||
|
||||
const skinview3d = await import('skinview3d');
|
||||
const skinViewer = new skinview3d.SkinViewer({
|
||||
width: 200,
|
||||
height: 300,
|
||||
renderPaused: true
|
||||
});
|
||||
|
||||
skinViewer.camera.rotation.x = -0.62;
|
||||
skinViewer.camera.rotation.y = 0.534;
|
||||
skinViewer.camera.rotation.z = 0.348;
|
||||
skinViewer.camera.position.x = 30.5;
|
||||
skinViewer.camera.position.y = 22.0;
|
||||
skinViewer.camera.position.z = 42.0;
|
||||
|
||||
await skinViewer.loadSkin(`https://mc-heads.net/skin/${value.username}`);
|
||||
skinViewer.render();
|
||||
skin = skinViewer.canvas.toDataURL();
|
||||
|
||||
skinViewer.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<span>Du hast Dich erfolgreich mit dem Team </span>
|
||||
<span class="inline-flex rounded-sm w-3 h-3" style="background-color: {$registeredPopupState?.teamColor}"></span>
|
||||
<span>{$registeredPopupState?.team}</span>
|
||||
<span> für Varo 4 registriert</span>. Spielstart ist am
|
||||
<i>
|
||||
{new Date(startDate).toLocaleString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' })}
|
||||
</i>
|
||||
um
|
||||
<i>
|
||||
{new Date(startDate).toLocaleString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr
|
||||
</i>.
|
||||
</p>
|
||||
<p class="text-center">Alle weiteren Informationen werden in der Whatsapp-Gruppe bekannt gegeben.</p>
|
||||
<p class="mt-2">
|
||||
Falls du uns unterstützen möchtest, kannst du dies ganz einfach über
|
||||
<a class="link" href={paypalLink} target="_blank">PayPal</a>
|
||||
tun. Antworten auf häufig gestellte Fragen findest du in unserer
|
||||
<a class="link" href="faq" target="_blank">FAQ</a>. Außerdem freuen wir uns, dich auf unserem
|
||||
<a class="link" href={teamspeakLink} target="_blank">TeamSpeak</a>
|
||||
oder in unserem
|
||||
<a class="link" href={discordLink} target="_blank">Discord</a>
|
||||
begrüßen zu dürfen!
|
||||
</p>
|
||||
<div class="divider"></div>
|
||||
<div class="flex justify-around mt-2 mb-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 w-full sm:w-fit gap-x-4 gap-y-2">
|
||||
<Input type="text" value={$registeredPopupState?.firstname} label="Vorname" disabled />
|
||||
<Input type="text" value={$registeredPopupState?.lastname} label="Nachname" disabled />
|
||||
<Input
|
||||
type="date"
|
||||
value={$registeredPopupState?.birthday.toISOString().substring(0, 10)}
|
||||
label="Geburtstag"
|
||||
size="sm"
|
||||
disabled
|
||||
/>
|
||||
<Input type="tel" value={$registeredPopupState?.phone} label="Telefonnummer" disabled />
|
||||
<Input type="text" value={$registeredPopupState?.username} label="Spielername" disabled />
|
||||
<Input type="text" value={$registeredPopupState?.teamMember} label="Mitspieler" disabled />
|
||||
</div>
|
||||
<div class="relative hidden md:flex justify-center w-[200px] my-4">
|
||||
{#if skin}
|
||||
<img class="absolute" src={skin} alt="" />
|
||||
{:else}
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="flex justify-center gap-8">
|
||||
<button class="btn">Weitere Person anmelden</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="absolute w-full h-full bg-black/50"></div>
|
||||
</dialog>
|
12
src/app/website/signup/RegisteredPopup.ts
Normal file
12
src/app/website/signup/RegisteredPopup.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export const registeredPopupState = atom<{
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
birthday: Date;
|
||||
phone: string;
|
||||
username: string;
|
||||
team: string;
|
||||
teamMember: string;
|
||||
teamColor: string;
|
||||
} | null>(null);
|
85
src/app/website/signup/RulesPopup.svelte
Normal file
85
src/app/website/signup/RulesPopup.svelte
Normal file
@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { rulesPopupState, rulesPopupRead } from './RulesPopup.ts';
|
||||
import { rules } from '../../../rules.ts';
|
||||
|
||||
const modalTimeoutSeconds = 30;
|
||||
|
||||
let modalElem: HTMLDialogElement;
|
||||
|
||||
let modalTimer = $state(null);
|
||||
let modalSecondsOpen = $state(import.meta.env.PROD ? 0 : modalTimeoutSeconds);
|
||||
|
||||
rulesPopupState.listen((value) => {
|
||||
if (value == 'open') {
|
||||
modalElem.show();
|
||||
setInterval(() => modalSecondsOpen++, 1000);
|
||||
} else if (value == 'closed') {
|
||||
clearInterval(modalTimer!);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<dialog
|
||||
id="rules-popup"
|
||||
class="modal"
|
||||
onclose={() => {
|
||||
if ($rulesPopupState !== 'accepted') $rulesPopupState = 'closed';
|
||||
}}
|
||||
bind:this={modalElem}
|
||||
>
|
||||
<form method="dialog" class="modal-box flex flex-col max-h-[90%] max-w-[95%] md:max-w-[90%] lg:max-w-[75%]">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
<div class="overflow-auto mt-5">
|
||||
<div class="mb-4">
|
||||
<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>
|
||||
{#each rules.sections as section, i (section.title)}
|
||||
<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">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
{@html section.content}
|
||||
</div>
|
||||
</div>
|
||||
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative w-min"
|
||||
title={modalSecondsOpen < modalTimeoutSeconds
|
||||
? `Regeln können in ${Math.max(modalTimeoutSeconds - modalSecondsOpen, 0)} Sekunden akzeptiert werden`
|
||||
: ''}
|
||||
>
|
||||
<!--div class="absolute top-0 left-0 h-full w-full overflow-hidden rounded-lg">
|
||||
<div
|
||||
style="width: {Math.min((modalSecondsOpen / modalTimeoutSeconds) * 100, 100)}%"
|
||||
class="h-full bg-base-300"
|
||||
></div>
|
||||
</div-->
|
||||
<button
|
||||
class="btn btn-neutral"
|
||||
disabled={modalSecondsOpen < modalTimeoutSeconds}
|
||||
onclick={() => {
|
||||
$rulesPopupRead = true;
|
||||
$rulesPopupState = 'accepted';
|
||||
}}>Akzeptieren</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
||||
<button class="!cursor-default">close</button>
|
||||
</form>
|
||||
</dialog>
|
4
src/app/website/signup/RulesPopup.ts
Normal file
4
src/app/website/signup/RulesPopup.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export const rulesPopupState = atom<'open' | 'closed' | 'accepted'>('closed');
|
||||
export const rulesPopupRead = atom(false);
|
30
src/app/website/signup/TeamPopup.svelte
Normal file
30
src/app/website/signup/TeamPopup.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { teamPopupOpen, teamPopupName } from '@app/website/signup/TeamPopup.ts';
|
||||
|
||||
let modal: HTMLDialogElement;
|
||||
let form: HTMLFormElement;
|
||||
|
||||
teamPopupOpen.subscribe((value) => {
|
||||
if (value) modal.show();
|
||||
else form?.reset();
|
||||
});
|
||||
</script>
|
||||
|
||||
<dialog class="modal" bind:this={modal} onclose={() => ($teamPopupOpen = false)}>
|
||||
<div class="modal-box">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</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>
|
||||
<button class="mt-4 btn btn-neutral">Team registrieren</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
4
src/app/website/signup/TeamPopup.ts
Normal file
4
src/app/website/signup/TeamPopup.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export const teamPopupOpen = atom(false);
|
||||
export const teamPopupName = atom<string | null>(null);
|
Reference in New Issue
Block a user