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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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()))));
|
||||||
|
|||||||
Reference in new issue
Block a user