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

+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import PlayerLink from './PlayerLink.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { import {
Landmark, ArrowDownToLine, ArrowUpFromLine, Wallet, Lock, Server, History, Coins, LoaderCircle, RefreshCw, TrendingUp Landmark, ArrowDownToLine, ArrowUpFromLine, Wallet, Lock, Server, History, Coins, LoaderCircle, RefreshCw, TrendingUp
@@ -213,7 +214,7 @@
</span> </span>
<Avatar uuid={tx.actor_uuid} name={m?.name ?? '?'} size={1.7} /> <Avatar uuid={tx.actor_uuid} name={m?.name ?? '?'} size={1.7} />
<div class="who"> <div class="who">
<strong>{m?.name ?? 'Former member'}</strong> <strong>{#if m}<PlayerLink uuid={tx.actor_uuid} name={m.name} />{:else}Former member{/if}</strong>
<span class="tiny muted" title={tx.note}>{verbs[tx.kind] ?? tx.kind}{tx.note ? ` · ${tx.note}` : ''} · {ago(tx.created_at)}</span> <span class="tiny muted" title={tx.note}>{verbs[tx.kind] ?? tx.kind}{tx.note ? ` · ${tx.note}` : ''} · {ago(tx.created_at)}</span>
</div> </div>
<span class="amt" class:out={isOut(tx.kind)}>{isOut(tx.kind) ? '−' : '+'}{money(tx.amount)}</span> <span class="amt" class:out={isOut(tx.kind)}>{isOut(tx.kind) ? '−' : '+'}{money(tx.amount)}</span>
@@ -2,6 +2,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { X, Send, UserPlus, Sparkles, MapPin, LoaderCircle, Shield, Clock, Swords, Star } from '@lucide/svelte'; import { X, Send, UserPlus, Sparkles, MapPin, LoaderCircle, Shield, Clock, Swords, Star } from '@lucide/svelte';
import Avatar from './Avatar.svelte'; import Avatar from './Avatar.svelte';
import PlayerLink from './PlayerLink.svelte';
import { activeAccount, toast } from '../lib/store.svelte'; import { activeAccount, toast } from '../lib/store.svelte';
import { invoke } from '../lib/tauri'; import { invoke } from '../lib/tauri';
import type { Guild, GuildMember, UserProfileView } from '../lib/types'; import type { Guild, GuildMember, UserProfileView } from '../lib/types';
@@ -84,7 +85,7 @@
<div class="head"> <div class="head">
<div class="ring"><Avatar {uuid} name={name} size={4} /></div> <div class="ring"><Avatar {uuid} name={name} size={4} /></div>
<div class="who"> <div class="who">
<h3>{name}</h3> <h3><PlayerLink {uuid} {name} /></h3>
{#if profile.level_info?.title}<span class="title"><Sparkles size={12} /> {profile.level_info.title}</span>{/if} {#if profile.level_info?.title}<span class="title"><Sparkles size={12} /> {profile.level_info.title}</span>{/if}
<span class="presence"><i class:on={online}></i>{online ? 'Online now' : 'Offline'}</span> <span class="presence"><i class:on={online}></i>{online ? 'Online now' : 'Offline'}</span>
</div> </div>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { app } from '../lib/store.svelte';
// A player's name that opens their profile. Safe to place inside other clickable rows.
let { uuid, name = '', class: cls = '', children }: { uuid?: string | null; name?: string; class?: string; children?: Snippet } = $props();
const open = (e: Event) => { e.stopPropagation(); if (uuid) app.viewProfileUuid = uuid; };
</script>
{#if uuid}
<button type="button" class="plink {cls}" title="View {name || 'player'}'s profile" onclick={open}
onkeydown={(e) => e.stopPropagation()}>{#if children}{@render children()}{:else}{name}{/if}</button>
{:else}
<span class={cls}>{#if children}{@render children()}{:else}{name}{/if}</span>
{/if}
<style>
.plink { all: unset; cursor: pointer; font: inherit; color: inherit; border-radius: 4px; transition: color 0.12s ease; }
.plink:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.plink:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
</style>
+4 -3
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { import {
Shield, Map, Users, MessageSquare, Plus, Crown, UserCheck, Trash2, Shield, Map, Users, MessageSquare, Plus, Crown, UserCheck, Trash2,
@@ -83,7 +84,7 @@
} }
} }
type Invite = { id: number; guild_id: string; guild_name: string; guild_tag: string; icon_url: string; inviter: string }; type Invite = { id: number; guild_id: string; guild_name: string; guild_tag: string; icon_url: string; inviter: string; inviter_uuid?: string };
let invites = $state<Invite[]>([]); let invites = $state<Invite[]>([]);
async function loadInvites() { async function loadInvites() {
try { invites = await invoke<Invite[]>('my_guild_invites'); } catch { invites = []; } try { invites = await invoke<Invite[]>('my_guild_invites'); } catch { invites = []; }
@@ -259,7 +260,7 @@
{#each invites as inv (inv.id)} {#each invites as inv (inv.id)}
<div class="invite"> <div class="invite">
<span class="dot"><Shield size={15} /></span> <span class="dot"><Shield size={15} /></span>
<span class="text"><strong>[{inv.guild_tag}] {inv.guild_name}</strong><span class="muted tiny">Invited by {inv.inviter}</span></span> <span class="text"><strong>[{inv.guild_tag}] {inv.guild_name}</strong><span class="muted tiny">Invited by <PlayerLink uuid={inv.inviter_uuid} name={inv.inviter} /></span></span>
<button class="sm ghost" onclick={() => answerInvite(inv, false)}>Decline</button> <button class="sm ghost" onclick={() => answerInvite(inv, false)}>Decline</button>
<button class="sm primary" onclick={() => answerInvite(inv, true)} disabled={!!myGuild} title={myGuild ? 'Leave your guild first' : ''}>Accept</button> <button class="sm primary" onclick={() => answerInvite(inv, true)} disabled={!!myGuild} title={myGuild ? 'Leave your guild first' : ''}>Accept</button>
</div> </div>
@@ -450,7 +451,7 @@
<div class="author-meta"> <div class="author-meta">
<Avatar uuid={p.author_uuid} name={p.author_name} size={1.8} /> <Avatar uuid={p.author_uuid} name={p.author_name} size={1.8} />
<div class="author-info"> <div class="author-info">
<span class="author-name">{p.author_name}</span> <span class="author-name"><PlayerLink uuid={p.author_uuid} name={p.author_name} /></span>
<span class="post-date tiny muted">{new Date(p.created_at).toLocaleString()}</span> <span class="post-date tiny muted">{new Date(p.created_at).toLocaleString()}</span>
</div> </div>
</div> </div>
+7 -6
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { import {
Users, UserPlus, MessageSquare, Gamepad2, Send, Check, X, Trash2, Users, UserPlus, MessageSquare, Gamepad2, Send, Check, X, Trash2,
@@ -301,7 +302,7 @@
<div class="card-left" role="button" tabindex="0" onclick={() => (app.viewProfileUuid = req.uuid)} onkeydown={(e) => e.key === 'Enter' && (app.viewProfileUuid = req.uuid)}> <div class="card-left" role="button" tabindex="0" onclick={() => (app.viewProfileUuid = req.uuid)} onkeydown={(e) => e.key === 'Enter' && (app.viewProfileUuid = req.uuid)}>
<Avatar uuid={req.uuid} name={req.username} size={2.5} /> <Avatar uuid={req.uuid} name={req.username} size={2.5} />
<div class="name-block"> <div class="name-block">
<span class="user-title">{req.username}</span> <span class="user-title"><PlayerLink uuid={req.uuid} name={req.username} /></span>
<span class="tiny muted">Wants to be friends</span> <span class="tiny muted">Wants to be friends</span>
</div> </div>
</div> </div>
@@ -326,7 +327,7 @@
<div class="card-left"> <div class="card-left">
<Avatar uuid={req.uuid} name={req.username} size={2.5} /> <Avatar uuid={req.uuid} name={req.username} size={2.5} />
<div class="name-block"> <div class="name-block">
<span class="user-title">{req.username}</span> <span class="user-title"><PlayerLink uuid={req.uuid} name={req.username} /></span>
<span class="tiny muted">Request pending...</span> <span class="tiny muted">Request pending...</span>
</div> </div>
</div> </div>
@@ -366,7 +367,7 @@
<span class="status-dot" class:online={friend.online}></span> <span class="status-dot" class:online={friend.online}></span>
</div> </div>
<div class="name-block"> <div class="name-block">
<span class="user-title">{friend.username}</span> <span class="user-title"><PlayerLink uuid={friend.uuid} name={friend.username} /></span>
{#if friend.online && friend.playing_on} {#if friend.online && friend.playing_on}
<span class="tiny playing-on">Playing on {friend.playing_on}</span> <span class="tiny playing-on">Playing on {friend.playing_on}</span>
{:else if friend.online} {:else if friend.online}
@@ -508,7 +509,7 @@
<span class="status-dot" class:online={friend.online}></span> <span class="status-dot" class:online={friend.online}></span>
</div> </div>
<div class="conv-meta"> <div class="conv-meta">
<span class="conv-name">{friend.username}</span> <span class="conv-name"><PlayerLink uuid={friend.uuid} name={friend.username} /></span>
{#if friend.playing_on} {#if friend.playing_on}
<span class="tiny muted">{friend.playing_on}</span> <span class="tiny muted">{friend.playing_on}</span>
{/if} {/if}
@@ -531,7 +532,7 @@
> >
<Avatar uuid={activeChatFriend.uuid} name={activeChatFriend.username} size={2.2} /> <Avatar uuid={activeChatFriend.uuid} name={activeChatFriend.username} size={2.2} />
<div> <div>
<strong>{activeChatFriend.username}</strong> <strong><PlayerLink uuid={activeChatFriend.uuid} name={activeChatFriend.username} /></strong>
<span class="tiny muted block">{activeChatFriend.online ? 'Active now' : 'Offline'}</span> <span class="tiny muted block">{activeChatFriend.online ? 'Active now' : 'Offline'}</span>
</div> </div>
</div> </div>
@@ -599,7 +600,7 @@
<div class="invite-top"> <div class="invite-top">
<Avatar uuid={inv.sender_uuid} name={inv.sender_name} size={2.5} /> <Avatar uuid={inv.sender_uuid} name={inv.sender_name} size={2.5} />
<div class="invite-meta"> <div class="invite-meta">
<span class="inviter-name"><strong>{inv.sender_name}</strong> invited you to play</span> <span class="inviter-name"><strong><PlayerLink uuid={inv.sender_uuid} name={inv.sender_name} /></strong> invited you to play</span>
<span class="server-title">{inv.server_name}</span> <span class="server-title">{inv.server_name}</span>
<span class="tiny muted server-addr">{inv.instance_name}</span> <span class="tiny muted server-addr">{inv.instance_name}</span>
</div> </div>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { import {
Trophy, Swords, Skull, Pickaxe, Clock, Box, MessageSquare, LogIn, Trophy, Swords, Skull, Pickaxe, Clock, Box, MessageSquare, LogIn,
RefreshCw, LoaderCircle, Users, Server, Flame, Sparkles, ChevronDown, Check, RefreshCw, LoaderCircle, Users, Server, Flame, Sparkles, ChevronDown, Check,
@@ -472,7 +473,7 @@
#{row.rank} #{row.rank}
</span> </span>
<Avatar uuid={row.uuid} name={row.username} size={2.2} /> <Avatar uuid={row.uuid} name={row.username} size={2.2} />
<strong class="baltop-name">{row.username}</strong> <strong class="baltop-name"><PlayerLink uuid={row.uuid} name={row.username} /></strong>
{#if isMe}<span class="badge you">You</span>{/if} {#if isMe}<span class="badge you">You</span>{/if}
</div> </div>
<span class="baltop-val highlight"> <span class="baltop-val highlight">
@@ -610,7 +611,7 @@
</span> </span>
<div class="col-player row align-center gap-sm"> <div class="col-player row align-center gap-sm">
<Avatar name={row.name} uuid={row.uuid} size={1.8} /> <Avatar name={row.name} uuid={row.uuid} size={1.8} />
<strong class="row-player-name">{row.name}</strong> <strong class="row-player-name"><PlayerLink uuid={row.uuid} name={row.name} /></strong>
{#if isMe}<span class="badge you">You</span>{/if} {#if isMe}<span class="badge you">You</span>{/if}
</div> </div>
<strong class="col-main highlight">{statValue(row, sortField)}</strong> <strong class="col-main highlight">{statValue(row, sortField)}</strong>
+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?)) 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 { fn invite_json(row: (i64, String, String, String, String, String, String, String)) -> Value {
let (id, guild_id, name, tag, icon, inviter, at) = row; 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, "created_at": at }) 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>> { async fn pending_for(state: &AppState, uuid: &str) -> AppResult<Vec<Value>> {
let rows: Vec<(i64, String, String, String, String, String, String)> = sqlx::query_as( 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 "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", 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) .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"> <script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { get } from '../lib/api'; 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 }; 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> <thead><tr><th>Time</th><th>Source</th><th>Player</th><th>Action</th><th>Details</th></tr></thead>
<tbody> <tbody>
{#each rows as row (row.source + ':' + row.id)} {#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} {:else}<tr><td colspan="5">{busy ? 'Loading…' : 'No matching activity yet.'}</td></tr>{/each}
</tbody> </tbody>
</table> </table>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Shield, Users, MapPin, Trash2, Search, ExternalLink, Flag } from '@lucide/svelte'; import { Shield, Users, MapPin, Trash2, Search, ExternalLink, Flag } from '@lucide/svelte';
import Modal from '../components/Modal.svelte'; import Modal from '../components/Modal.svelte';
@@ -159,7 +160,7 @@
<code class="instance-code">{g.instance_id}</code> <code class="instance-code">{g.instance_id}</code>
</td> </td>
<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>
<td> <td>
<span class="count-pill"> <span class="count-pill">
@@ -228,7 +229,7 @@
.guild-desc { margin: 2px 0 0; font-size: 0.78rem; color: var(--muted); } .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); } .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); } .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; } .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"> <script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Sparkles, Trophy, Plus, Pencil, Trash2, Award, Shield, Crown } from '@lucide/svelte'; import { Sparkles, Trophy, Plus, Pencil, Trash2, Award, Shield, Crown } from '@lucide/svelte';
import Modal from '../components/Modal.svelte'; import Modal from '../components/Modal.svelte';
@@ -192,7 +193,7 @@
<div class="lb-item"> <div class="lb-item">
<span class="lb-rank">#{i + 1}</span> <span class="lb-rank">#{i + 1}</span>
<div class="lb-info"> <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> <span>{Number((p as any).global_xp ?? p.current_xp ?? 0).toLocaleString()} XP</span>
</div> </div>
<span class="lb-lvl">Lvl {(p as any).global_level ?? p.level}</span> <span class="lb-lvl">Lvl {(p as any).global_level ?? p.level}</span>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import PlayerLink from '../components/PlayerLink.svelte';
import { import {
ArrowLeft, Map as MapIcon, Settings2, KeyRound, Trash2, Users, Gauge, Clock, UserRound, LogIn, LogOut, Skull, Award, Sword, MessageSquare, ArrowLeft, Map as MapIcon, Settings2, KeyRound, Trash2, Users, Gauge, Clock, UserRound, LogIn, LogOut, Skull, Award, Sword, MessageSquare,
CircleDot, Ban, Server, CircleDot, Ban, Server,
@@ -173,7 +174,7 @@
{#each d.leaderboard as row, i (row.uuid)} {#each d.leaderboard as row, i (row.uuid)}
<tr> <tr>
<td class="rank" class:top={i < 3 && sort !== 'last_seen'}>{i + 1}</td> <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} {#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> <td class="muted small nowrap">{d.online_players.some((p) => p.uuid === row.uuid) ? 'Online' : timeAgo(row.last_seen)}</td>
</tr> </tr>
@@ -192,7 +193,7 @@
{#if d.online_players.length} {#if d.online_players.length}
<div class="online"> <div class="online">
{#each d.online_players as p (p.uuid)} {#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} {/each}
</div> </div>
{:else} {:else}
+10
View File
@@ -5,6 +5,7 @@
import PlayerPanel from '../components/PlayerPanel.svelte'; import PlayerPanel from '../components/PlayerPanel.svelte';
import { del, duration, get, patch, post, put, timeAgo } from '../lib/api'; import { del, duration, get, patch, post, put, timeAgo } from '../lib/api';
import { session } from '../lib/session.svelte'; import { session } from '../lib/session.svelte';
import { route, go } from '../lib/router.svelte';
import { toast, toastError } from '../lib/toast.svelte'; import { toast, toastError } from '../lib/toast.svelte';
import type { Cape, Group, User } from '../lib/types'; 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; const lastSeen = (u: User) => [u.last_login, u.last_seen_ingame].filter(Boolean).sort().at(-1) ?? null;
$effect(refresh); $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 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())))); const shown = $derived(users.filter((u) => !filter || u.username.toLowerCase().includes(filter.toLowerCase()) || u.groups.some((g) => g.toLowerCase().includes(filter.toLowerCase()))));