Add admin panel web UI (Svelte 5)
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
This commit is contained in:
32 files changed
+4084
No files matched your search
@@ -0,0 +1,59 @@
|
||||
<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>
|
||||
Reference in new issue
Block a user