beeeeg update
Some checks failed
Test and deploy staging / staging (push) Has been cancelled

This commit is contained in:
2026-09-23 04:15:10 +00:00
parent 487eff5bb2
commit d0c7b6548b
135 changed files with 9531 additions and 1777 deletions

View File

@@ -0,0 +1,149 @@
<script lang="ts">
import { authClient } from '$lib/auth-client';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { resolve } from '$app/paths';
let {
mode,
providers,
returnTo,
oauthError
}: {
mode: 'login' | 'register';
providers: { google: boolean; osu: boolean };
returnTo: string;
oauthError: boolean;
} = $props();
let name = $state('');
let email = $state('');
let password = $state('');
let busy = $state(false);
let ready = $state(false);
onMount(() => {
ready = true;
});
let message = $state('');
async function submit(event: SubmitEvent) {
event.preventDefault();
const destination = `${resolve('/')}${returnTo.slice(1)}`;
busy = true;
message = '';
try {
const result =
mode === 'register'
? await authClient.signUp.email({ name: name.trim(), email, password })
: await authClient.signIn.email({ email, password });
if (result.error) {
message = result.error.message || 'Unable to sign in. Please try again.';
return;
}
// Destination is a validated local path, prefixed by resolve('/') above.
// eslint-disable-next-line svelte/no-navigation-without-resolve
await goto(destination, { invalidateAll: true });
} catch {
message = 'Unable to connect. Please try again.';
} finally {
busy = false;
}
}
async function social(provider: 'google' | 'osu') {
busy = true;
message = '';
try {
const result = await authClient.signIn.social({
provider,
callbackURL: returnTo,
errorCallbackURL: `/login?returnTo=${encodeURIComponent(returnTo)}`
});
if (result.error) {
message = result.error.message || 'Unable to connect to this provider.';
busy = false;
}
} catch {
message = 'Unable to connect. Please try again.';
busy = false;
}
}
</script>
<section class="mx-auto my-12 max-w-md px-4 sm:my-20">
<h1 class="mb-8 text-3xl font-semibold tracking-tight">
{mode === 'register' ? 'create account' : 'sign in'}
</h1>
{#if message || oauthError}
<p role="alert" class="mb-4 rounded bg-red-50 p-3 text-red-800">
{message ||
'Provider sign-in failed or was cancelled. Try again, or use your existing login and link the provider from your account.'}
</p>
{/if}
<form method="POST" onsubmit={submit} class="flex flex-col gap-4">
{#if mode === 'register'}
<label class="flex flex-col gap-1"
>Name<input
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
disabled={!ready || busy}
name="name"
autocomplete="name"
required
maxlength="100"
bind:value={name}
/></label
>
{/if}
<label class="flex flex-col gap-1"
>Email<input
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
disabled={!ready || busy}
name="email"
type="email"
autocomplete="email"
required
bind:value={email}
/></label
>
<label class="flex flex-col gap-1"
>Password<input
class="rounded border border-[var(--site-border)] bg-[var(--site-bg)] px-3 py-2"
disabled={!ready || busy}
name="password"
type="password"
autocomplete={mode === 'register' ? 'new-password' : 'current-password'}
required
minlength={mode === 'register' ? 12 : undefined}
maxlength="128"
bind:value={password}
/></label
>
{#if mode === 'register'}<p class="text-sm text-gray-600">Use at least 12 characters.</p>{/if}
<button
disabled={!ready || busy}
class="rounded bg-[var(--site-text)] px-4 py-2 font-semibold text-[var(--site-bg)] disabled:opacity-50"
>{busy ? 'please wait…' : mode === 'register' ? 'create account' : 'sign in'}</button
>
</form>
{#if providers.google || providers.osu}
<div class="my-6 flex flex-col gap-3 border-t border-[var(--site-border)] pt-4">
{#if providers.google}<button
disabled={!ready || busy}
onclick={() => social('google')}
class="rounded border border-[var(--site-border)] px-4 py-2 disabled:opacity-50"
>Continue with Google</button
>{/if}
{#if providers.osu}<button
disabled={!ready || busy}
onclick={() => social('osu')}
class="rounded border border-[var(--site-border)] px-4 py-2 disabled:opacity-50"
>Continue with osu!</button
>{/if}
</div>
{/if}
<p class="mt-6 text-base">
<a
class="underline underline-offset-4"
href={resolve(
`${mode === 'register' ? '/login' : '/register'}?returnTo=${encodeURIComponent(returnTo)}`
)}>{mode === 'register' ? 'sign in' : 'Create account'}</a
>
</p>
<noscript><p class="mt-4">Enable JavaScript to securely sign in or register.</p></noscript>
</section>

View File

@@ -33,7 +33,7 @@
<button
onclick={copy}
aria-label={`Copy ${label}: ${value}`}
class="text-2xl transition hover:opacity-60 cursor-pointer"
class="flex h-10 w-10 cursor-pointer items-center justify-center text-xl transition hover:text-[var(--site-text)]"
>
<Icon />
</button>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { onMount } from 'svelte';
let { label = 'Picture (optional)' }: { label?: string } = $props();
let input: HTMLInputElement;
let fileName = $state('');
let dragging = $state(false);
function selected() {
fileName = input.files?.[0]?.name ?? '';
}
function drop(event: DragEvent) {
event.preventDefault();
dragging = false;
const file = event.dataTransfer?.files?.[0];
if (!file) return;
const transfer = new DataTransfer();
transfer.items.add(file);
input.files = transfer.files;
selected();
}
function clear() {
input.value = '';
fileName = '';
}
onMount(() => {
const form = input.form;
form?.addEventListener('reset', clear);
return () => form?.removeEventListener('reset', clear);
});
</script>
<div>
<span class="block font-medium">{label}</span>
<input
bind:this={input}
name="picture"
type="file"
accept="image/jpeg,image/png,image/webp"
onchange={selected}
class="sr-only"
aria-label={label}
/>
<button
type="button"
onclick={() => input.click()}
ondragover={(event) => {
event.preventDefault();
dragging = true;
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
}}
ondragleave={() => (dragging = false)}
ondrop={drop}
class={`mt-2 block w-full rounded border border-dashed p-5 text-center transition-colors ${dragging ? 'border-[var(--site-text)] bg-[var(--site-hover)]' : 'border-[var(--site-border)] hover:border-[var(--site-muted)]'}`}
>
{fileName ? `Selected: ${fileName}` : 'Drop an image here or click to browse'}
</button>
<p class="mt-2 text-base text-[var(--site-muted)]">JPEG, PNG, or WebP · up to 5 MB</p>
{#if fileName}<button type="button" onclick={clear} class="mt-2 underline">Clear selection</button
>{/if}
</div>

View File

@@ -0,0 +1,43 @@
<script lang="ts">
let {
value,
max,
label,
small = false
}: {
value: number;
max: number;
label: string;
small?: boolean;
} = $props();
</script>
<progress class:small {value} max={max || 1} aria-label={label}></progress>
<style>
progress {
display: block;
width: 100%;
height: 0.625rem;
appearance: none;
border: 0;
border-radius: 9999px;
overflow: hidden;
background: var(--site-border);
}
progress.small {
height: 0.375rem;
}
progress::-webkit-progress-bar {
background: var(--site-border);
border-radius: 9999px;
}
progress::-webkit-progress-value {
background: var(--site-text);
border-radius: 9999px;
}
progress::-moz-progress-bar {
background: var(--site-text);
border-radius: 9999px;
}
</style>