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:
13 files changed
+82
-24
No files matched your search
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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()))));
|
||||
|
||||
Reference in new issue
Block a user