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,80 @@
|
||||
<script lang="ts">
|
||||
import Sidebar from './components/Sidebar.svelte';
|
||||
import Toasts from './components/Toasts.svelte';
|
||||
import Login from './pages/Login.svelte';
|
||||
import Dashboard from './pages/Dashboard.svelte';
|
||||
import Instances from './pages/Instances.svelte';
|
||||
import InstanceEditor from './pages/InstanceEditor.svelte';
|
||||
import Users from './pages/Users.svelte';
|
||||
import Branding from './pages/Branding.svelte';
|
||||
import Settings from './pages/Settings.svelte';
|
||||
import { get } from './lib/api';
|
||||
import { route } from './lib/router.svelte';
|
||||
import { logout, session, type Me } from './lib/session.svelte';
|
||||
import { toast } from './lib/toast.svelte';
|
||||
|
||||
let brand = $state<{ name: string; logo_url: string | null }>({ name: 'ScopeNet', logo_url: null });
|
||||
let checking = $state(!!session.token);
|
||||
|
||||
$effect(() => {
|
||||
if (!session.token) {
|
||||
checking = false;
|
||||
return;
|
||||
}
|
||||
get<Me>('/api/v1/auth/me')
|
||||
.then((me) => {
|
||||
if (me.role !== 'admin') {
|
||||
toast('That account is not an admin.', 'error');
|
||||
logout();
|
||||
} else session.user = me;
|
||||
})
|
||||
.catch(() => logout())
|
||||
.finally(() => (checking = false));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
get('/api/v1/launcher/manifest')
|
||||
.then((m) => {
|
||||
brand = m.branding;
|
||||
document.title = `${m.branding.name} · Admin`;
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if checking}
|
||||
<div class="boot"><span class="pulse"></span></div>
|
||||
{:else if !session.user}
|
||||
<Login brandName={brand.name} logo={brand.logo_url} />
|
||||
{:else}
|
||||
<div class="shell">
|
||||
<Sidebar brandName={brand.name} logo={brand.logo_url} />
|
||||
<main>
|
||||
{#key route.name + route.params.join('/')}
|
||||
{#if route.name === 'instances' && route.params[0]}
|
||||
<InstanceEditor id={route.params[0]} />
|
||||
{:else if route.name === 'instances'}
|
||||
<Instances />
|
||||
{:else if route.name === 'users'}
|
||||
<Users />
|
||||
{:else if route.name === 'branding'}
|
||||
<Branding onsaved={(b) => (brand = b)} />
|
||||
{:else if route.name === 'settings'}
|
||||
<Settings />
|
||||
{:else}
|
||||
<Dashboard />
|
||||
{/if}
|
||||
{/key}
|
||||
</main>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Toasts />
|
||||
|
||||
<style>
|
||||
.shell { display: flex; min-height: 100vh; }
|
||||
main { flex: 1; min-width: 0; }
|
||||
.boot { height: 100vh; display: grid; place-items: center; }
|
||||
.pulse { width: 42px; height: 42px; border-radius: 12px; background: linear-gradient(135deg, var(--accent), var(--accent-2)); animation: pulse 1s ease-in-out infinite alternate; }
|
||||
@keyframes pulse { from { transform: scale(0.85); opacity: 0.6; } to { transform: scale(1); opacity: 1; } }
|
||||
</style>
|
||||
Reference in new issue
Block a user