move components

This commit is contained in:
2025-05-20 20:34:50 +02:00
parent b610b30a78
commit 7e6a09563a
14 changed files with 16 additions and 27 deletions

179
src/app/layout/Menu.svelte Normal file
View File

@ -0,0 +1,179 @@
<script lang="ts">
import MenuHome from '@assets/img/menu-home.webp';
import MenuSignup from '@assets/img/menu-signup.webp';
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 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';
import { isBrowser } from '@antfu/utils';
import { navigate } from 'astro:transitions/client';
import { onMount } from 'svelte';
let navPaths = $state([
{
name: 'Startseite',
sprite: MenuHome.src,
href: ``,
active: false
},
{
name: 'Registrieren',
sprite: MenuSignup.src,
href: 'signup',
active: false
},
{
name: 'Regeln',
sprite: MenuRules.src,
href: 'rules',
active: false
},
{
name: 'FAQ',
sprite: MenuFaq.src,
href: 'faq',
active: false
},
{
name: 'Feedback & Kontakt',
sprite: MenuFeedback.src,
href: 'feedback',
active: false
},
{
name: 'Team',
sprite: MenuTeam.src,
href: 'team',
active: false
}
]);
let showMenuPermanent = $state(isBrowser ? localStorage.getItem('showMenuPermanent') === 'true' : false);
let isTouch = $state(false);
let isOpen = $state(false);
let windowHeight = $state(0);
$effect(() => {
localStorage.setItem('showMenuPermanent', `${showMenuPermanent}`);
});
onMount(() => {
new MutationObserver(() => {
for (let i = 0; i < navPaths.length; i++) {
console.log(navPaths[i].href, window.location.pathname);
navPaths[i].active = new URL(navPaths[i].href).pathname === window.location.pathname;
}
}).observe(document.head, { childList: true });
});
let navElem: HTMLDivElement;
</script>
<svelte:window bind:innerHeight={windowHeight} />
<svelte:body
ontouchend={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (isTouch && !navElem.contains(e.target)) showMenuPermanent = false;
}}
/>
<div
class="fixed bottom-4 right-4 sm:left-4 sm:right-[initial] group/menu-bar flex flex-col-reverse justify-center items-center z-50 main-menu"
bind:this={navElem}
>
<button
class={isTouch ? 'btn btn-square relative w-16 h-16' : 'btn btn-square group/menu-button relative w-16 h-16'}
onclick={() => {
if (!isTouch) {
let activePath = navPaths.find((path) => path.active);
if (activePath !== undefined) {
navigate(activePath.href);
}
showMenuPermanent = !showMenuPermanent;
}
}}
ontouchend={() => {
isTouch = true;
showMenuPermanent = !showMenuPermanent;
}}
>
<img class="absolute w-full h-full p-1 pixelated" src={MenuButton.src} alt="menu" />
<img
class="opacity-0 transition-opacity delay-50 group-hover/menu-button:opacity-100 absolute w-full h-full p-[3px] pixelated"
class:opacity-100={isOpen || (isTouch && showMenuPermanent)}
src={MenuSelectedFrame.src}
alt="menu hover"
/>
</button>
<div
class:hidden={!(isOpen || showMenuPermanent)}
class={isTouch ? 'pb-3' : 'group-hover/menu-bar:block pb-3'}
onmouseenter={() => (isOpen = true)}
onmouseleave={() => (isOpen = false)}
>
<ul class="bg-base-200 rounded">
{#each navPaths as navPath, i (navPath.href)}
<li
class="flex justify-center tooltip"
class:tooltip-left={windowHeight > 450}
class:sm:tooltip-right={windowHeight > 450}
class:tooltip-top={windowHeight <= 450}
class:tooltip-open={isTouch || windowHeight <= 450}
data-tip={navPath.name}
>
<a
class="btn btn-square border-none group/menu-item relative w-[3.5rem] h-[3.5rem] flex justify-center items-center"
href={navPath.href}
onclick={() => navigate(navPath.href)}
>
<div
style="background-image: url({MenuInventoryBar.src}); background-position: -{i * 3.5}rem 0;"
class="block w-full h-full bg-no-repeat bg-horizontal-sprite pixelated"
></div>
<div class="absolute flex justify-center items-center w-full h-full">
<img class="w-1/2 h-1/2 pixelated" src={navPath.sprite} alt={navPath.name} />
</div>
<img
class="transition-opacity delay-50 group-hover/menu-item:opacity-100 absolute w-full h-full pixelated scale-110 z-10"
class:opacity-0={!navPath.active}
src={MenuSelectedFrame.src}
alt="menu hover"
/>
</a>
</li>
{/each}
</ul>
</div>
</div>
<style>
@media (max-height: 450px) {
.main-menu {
flex-direction: row;
}
.main-menu > div {
padding: 0.25rem 0 0 0.5rem;
}
.main-menu li {
display: inline-block;
&::before {
transform-origin: 0;
transform: rotate(-90deg);
margin-bottom: -0.5rem;
}
}
}
.pixelated {
image-rendering: pixelated;
}
.bg-horizontal-sprite {
background-size: auto 100%;
}
</style>

View 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>

View 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>

View File

@ -1,6 +1,5 @@
<script lang="ts">
import Steve from '@assets/img/steve.png';
import Team from '@components/website/Team.svelte';
import type { GetDeathsRes } from '@db/schema/death.ts';
import { type ActionReturnType, actions } from 'astro:actions';
@ -26,7 +25,10 @@
{#each teams as team (team.id)}
<tr>
<td>
<Team name={team.name} color={team.color} />
<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}

View 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&nbsp;&nbsp;</span>
<span class="inline-flex rounded-sm w-3 h-3" style="background-color: {$registeredPopupState?.teamColor}"></span>
<span>{$registeredPopupState?.team}</span>
<span>&nbsp;&nbsp;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>

View 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);

View 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>

View File

@ -0,0 +1,4 @@
import { atom } from 'nanostores';
export const rulesPopupState = atom<'open' | 'closed' | 'accepted'>('closed');
export const rulesPopupRead = atom(false);

View 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>

View File

@ -0,0 +1,4 @@
import { atom } from 'nanostores';
export const teamPopupOpen = atom(false);
export const teamPopupName = atom<string | null>(null);