Player names open profiles across the launcher and admin panel

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
Claude committed 2026-10-01 00:49:13 +00:00
1 parent f428bd210f
commit c37cde5cc8
13 files changed
+82 -24

No files matched your search

+5 -5
View File
@@ -200,14 +200,14 @@ pub async fn send_invite(auth: AuthUser, Path(guild_id): Path<String>, State(sta
Ok(Json(create_invite(&state, &auth.uuid, &auth.username, &guild_id, &p.uuid).await?))
}
fn invite_json(row: (i64, String, String, String, String, String, String)) -> Value {
let (id, guild_id, name, tag, icon, inviter, at) = row;
json!({ "id": id, "guild_id": guild_id, "guild_name": name, "guild_tag": tag, "icon_url": icon, "inviter": inviter, "created_at": at })
fn invite_json(row: (i64, String, String, String, String, String, String, String)) -> Value {
let (id, guild_id, name, tag, icon, inviter, at, inviter_uuid) = row;
json!({ "id": id, "guild_id": guild_id, "guild_name": name, "guild_tag": tag, "icon_url": icon, "inviter": inviter, "inviter_uuid": inviter_uuid, "created_at": at })
}
async fn pending_for(state: &AppState, uuid: &str) -> AppResult<Vec<Value>> {
let rows: Vec<(i64, String, String, String, String, String, String)> = sqlx::query_as(
"SELECT i.id, i.guild_id, g.name, g.tag, COALESCE(g.icon_url, ''), i.inviter_name, i.created_at
let rows: Vec<(i64, String, String, String, String, String, String, String)> = sqlx::query_as(
"SELECT i.id, i.guild_id, g.name, g.tag, COALESCE(g.icon_url, ''), i.inviter_name, i.created_at, i.inviter_uuid
FROM guild_invites i JOIN guilds g ON g.id = i.guild_id WHERE i.target_uuid = ? AND i.status = 'pending' ORDER BY i.id DESC LIMIT 20",
)
.bind(uuid)
@@ -0,0 +1,18 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { go } from '../lib/router.svelte';
// A player's name that opens their profile in the Players page.
let { uuid, name = '', children }: { uuid?: string | null; name?: string; children?: Snippet } = $props();
</script>
{#if uuid}
<a class="plink" href={`#/users/${uuid}`} title="View {name || 'player'}'s profile" onclick={(e) => { e.preventDefault(); go(`users/${uuid}`); }}>{#if children}{@render children()}{:else}{name}{/if}</a>
{:else}
{#if children}{@render children()}{:else}{name}{/if}
{/if}
<style>
.plink { color: inherit; text-decoration: none; border-radius: 4px; }
.plink:hover { color: var(--accent, #8b6cff); text-decoration: underline; text-underline-offset: 3px; }
</style>
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte';
import { get } from '../lib/api';
type Entry = { id: number; source: string; server: string | null; uuid: string | null; name: string | null; kind: string; detail: string | null; created_at: string };
@@ -32,7 +33,7 @@
<thead><tr><th>Time</th><th>Source</th><th>Player</th><th>Action</th><th>Details</th></tr></thead>
<tbody>
{#each rows as row (row.source + ':' + row.id)}
<tr><td>{new Date(row.created_at).toLocaleString()}</td><td>{row.server ?? row.source}</td><td title={row.uuid ?? ''}>{row.name ?? 'Server'}</td><td>{row.kind.replaceAll('_', ' ')}</td><td>{row.detail ?? '—'}</td></tr>
<tr><td>{new Date(row.created_at).toLocaleString()}</td><td>{row.server ?? row.source}</td><td title={row.uuid ?? ''}>{#if row.uuid && row.name}<PlayerLink uuid={row.uuid} name={row.name} />{:else}{row.name ?? 'Server'}{/if}</td><td>{row.kind.replaceAll('_', ' ')}</td><td>{row.detail ?? '—'}</td></tr>
{:else}<tr><td colspan="5">{busy ? 'Loading…' : 'No matching activity yet.'}</td></tr>{/each}
</tbody>
</table>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte';
import { Shield, Users, MapPin, Trash2, Search, ExternalLink, Flag } from '@lucide/svelte';
import Modal from '../components/Modal.svelte';
@@ -159,7 +160,7 @@
<code class="instance-code">{g.instance_id}</code>
</td>
<td>
<code class="uuid-code">{g.leader_uuid.slice(0, 8)}...</code>
<PlayerLink uuid={g.leader_uuid} name={g.leader_uuid.slice(0, 8) + '…'} />
</td>
<td>
<span class="count-pill">
@@ -228,7 +229,7 @@
.guild-desc { margin: 2px 0 0; font-size: 0.78rem; color: var(--muted); }
.tag-badge { font-family: monospace; font-weight: 700; color: #a5b4fc; background: rgba(99,102,241,0.12); padding: 2px 8px; border-radius: 4px; border: 1px solid rgba(99,102,241,0.25); }
.instance-code, .uuid-code { font-size: 0.8rem; color: var(--muted); background: rgba(255,255,255,0.04); padding: 2px 6px; border-radius: 4px; }
.instance-code { font-size: 0.8rem; color: var(--muted); background: rgba(255,255,255,0.04); padding: 2px 6px; border-radius: 4px; }
.count-pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.85rem; color: var(--text-2); }
.claims-pill { display: inline-flex; align-items: center; gap: 6px; font-size: 0.85rem; color: #4ade80; }
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte';
import { Sparkles, Trophy, Plus, Pencil, Trash2, Award, Shield, Crown } from '@lucide/svelte';
import Modal from '../components/Modal.svelte';
@@ -192,7 +193,7 @@
<div class="lb-item">
<span class="lb-rank">#{i + 1}</span>
<div class="lb-info">
<strong>{(p as any).username ?? `UUID: ${p.uuid.slice(0, 8)}…`}</strong>
<strong><PlayerLink uuid={p.uuid} name={(p as any).username ?? `UUID: ${p.uuid.slice(0, 8)}…`} /></strong>
<span>{Number((p as any).global_xp ?? p.current_xp ?? 0).toLocaleString()} XP</span>
</div>
<span class="lb-lvl">Lvl {(p as any).global_level ?? p.level}</span>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import {
ArrowLeft, Map as MapIcon, Settings2, KeyRound, Trash2, Users, Gauge, Clock, UserRound, LogIn, LogOut, Skull, Award, Sword, MessageSquare,
CircleDot, Ban, Server,
@@ -173,7 +174,7 @@
{#each d.leaderboard as row, i (row.uuid)}
<tr>
<td class="rank" class:top={i < 3 && sort !== 'last_seen'}>{i + 1}</td>
<td><span class="row who"><Avatar name={row.name} uuid={row.uuid} size={24} />{row.name}</span></td>
<td><span class="row who"><Avatar name={row.name} uuid={row.uuid} size={24} /><PlayerLink uuid={row.uuid} name={row.name} /></span></td>
{#each columns as c}<td class="num" class:sorted={sort === c.id}>{cell(row, c.id)}</td>{/each}
<td class="muted small nowrap">{d.online_players.some((p) => p.uuid === row.uuid) ? 'Online' : timeAgo(row.last_seen)}</td>
</tr>
@@ -192,7 +193,7 @@
{#if d.online_players.length}
<div class="online">
{#each d.online_players as p (p.uuid)}
<div class="row op"><Avatar name={p.name} uuid={p.uuid} size={26} /><span class="grow">{p.name}</span><span class="muted tiny">{timeAgo(p.joined_at).replace(' ago', '')}</span></div>
<div class="row op"><Avatar name={p.name} uuid={p.uuid} size={26} /><span class="grow"><PlayerLink uuid={p.uuid} name={p.name} /></span><span class="muted tiny">{timeAgo(p.joined_at).replace(' ago', '')}</span></div>
{/each}
</div>
{:else}
+10
View File
@@ -5,6 +5,7 @@
import PlayerPanel from '../components/PlayerPanel.svelte';
import { del, duration, get, patch, post, put, timeAgo } from '../lib/api';
import { session } from '../lib/session.svelte';
import { route, go } from '../lib/router.svelte';
import { toast, toastError } from '../lib/toast.svelte';
import type { Cape, Group, User } from '../lib/types';
@@ -38,6 +39,15 @@
}
const lastSeen = (u: User) => [u.last_login, u.last_seen_ingame].filter(Boolean).sort().at(-1) ?? null;
$effect(refresh);
// #/users/<uuid> (a player's name anywhere in the panel) opens their profile.
$effect(() => {
const target = route.params[0];
if (target && users.length) {
const found = users.find((x) => x.uuid === target);
if (found) openView(found);
}
});
$effect(() => { if (!viewOpen && route.params[0]) go('users'); });
const pending = $derived(users.filter((u) => u.status === 'pending'));
const shown = $derived(users.filter((u) => !filter || u.username.toLowerCase().includes(filter.toLowerCase()) || u.groups.some((g) => g.toLowerCase().includes(filter.toLowerCase()))));