Dashboard with launch stats, instance manager with Modrinth/CurseForge search and zip import, player and group management, launcher design editor with live preview, and sign-in settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
60 lines
2.9 KiB
Svelte
60 lines
2.9 KiB
Svelte
<script lang="ts">
|
|
import { LogIn, LoaderCircle } from '@lucide/svelte';
|
|
import { post } from '../lib/api';
|
|
import { setToken } from '../lib/session.svelte';
|
|
|
|
let { brandName, logo }: { brandName: string; logo: string | null } = $props();
|
|
let username = $state('');
|
|
let password = $state('');
|
|
let error = $state('');
|
|
let busy = $state(false);
|
|
|
|
async function submit(e: SubmitEvent) {
|
|
e.preventDefault();
|
|
busy = true;
|
|
error = '';
|
|
try {
|
|
const r = await post<{ token: string; user: { role: string } }>('/api/v1/auth/login', { username, password });
|
|
if (r.user.role !== 'admin') throw new Error('This account is not an administrator.');
|
|
setToken(r.token);
|
|
} catch (err) {
|
|
error = err instanceof Error ? err.message : String(err);
|
|
} finally {
|
|
busy = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="wrap">
|
|
<div class="orb a"></div>
|
|
<div class="orb b"></div>
|
|
<form class="card login" onsubmit={submit}>
|
|
<img src={logo ?? '/favicon.svg'} alt="" class="logo" />
|
|
<h1>{brandName}</h1>
|
|
<p class="muted">Sign in to manage your launcher</p>
|
|
<label class="field">Username<input bind:value={username} autocomplete="username" required /></label>
|
|
<label class="field">Password<input type="password" bind:value={password} autocomplete="current-password" required /></label>
|
|
{#if error}<div class="error">{error}</div>{/if}
|
|
<button class="primary big" disabled={busy}>
|
|
{#if busy}<LoaderCircle class="spin" size={18} />{:else}<LogIn size={18} />{/if}
|
|
Sign in
|
|
</button>
|
|
<p class="tiny muted hint">First start? The admin password is in the container logs (or set <code>ADMIN_PASSWORD</code>).</p>
|
|
</form>
|
|
</div>
|
|
|
|
<style>
|
|
.wrap { min-height: 100vh; display: grid; place-items: center; padding: 20px; position: relative; overflow: hidden; }
|
|
.orb { position: absolute; width: 520px; height: 520px; border-radius: 50%; filter: blur(90px); opacity: 0.35; animation: drift 14s ease-in-out infinite alternate; }
|
|
.orb.a { background: var(--accent); top: -160px; right: -120px; }
|
|
.orb.b { background: var(--accent-2); bottom: -200px; left: -160px; opacity: 0.2; animation-delay: -7s; }
|
|
@keyframes drift { to { transform: translate(-60px, 40px) scale(1.1); } }
|
|
.login { width: 100%; max-width: 400px; display: flex; flex-direction: column; gap: 16px; padding: 36px; position: relative; backdrop-filter: blur(20px); background: rgba(18, 20, 31, 0.8); box-shadow: var(--shadow); animation: fade-in 0.4s ease; }
|
|
.logo { width: 56px; height: 56px; border-radius: 16px; margin-bottom: 4px; object-fit: cover; }
|
|
h1 { font-size: 1.6rem; }
|
|
.muted { margin-top: -10px; }
|
|
.big { padding: 12px; margin-top: 6px; }
|
|
.error { background: rgba(244, 63, 94, 0.1); border: 1px solid rgba(244, 63, 94, 0.3); color: #fda4af; padding: 10px 12px; border-radius: 10px; font-size: 0.88rem; }
|
|
.hint { text-align: center; margin-top: 0; line-height: 1.5; }
|
|
</style>
|