profile-window.svelte RawHistoryFolder

1<script lang="ts">
2	import { useQuery } from 'convex-svelte';
3	import { api } from '../../../convex/_generated/api';
4	import type { Id } from '../../../convex/_generated/dataModel';
5	import XpWindow from './xp-window.svelte';
6	import { desktop } from '$lib/states/desktop.svelte';
7	import { chatState } from '$lib/states/chat.svelte';
8	import { useAimClient } from '$lib/aim/client';
9	import { STATUS_LABELS_FR } from '$lib/types/aim';
10	import { formatFrenchDateTime } from '$lib/utils/date-format';
11
12	let { userId, nickname } = $props<{ userId: Id<'users'>; nickname: string }>();
13	const id = $derived(`profile-${userId}`);
14	const profileQuery = useQuery(api.aim.getUserProfile, () => ({ userId }));
15	const client = useAimClient();
16	const profile = $derived(profileQuery.data);
17	const own = $derived(userId === chatState.getCurrentUser()?.id);
18	let text = $state('');
19	let saved = $state(false);
20	let saving = $state(false);
21	let error = $state('');
22	let initializedFor = $state<string | null>(null);
23	$effect(() => {
24		if (profile && initializedFor !== userId) {
25			text = profile.profile ?? '';
26			initializedFor = userId;
27		}
28	});
29
30	async function save() {
31		saving = true;
32		saved = false;
33		error = '';
34		try {
35			await client.updateProfile({ profile: text });
36			saved = true;
37		} catch (err) {
38			error = err instanceof Error ? err.message : 'Impossible d’enregistrer.';
39		} finally {
40			saving = false;
41		}
42	}
43</script>
44
45<XpWindow {id} width={380} height={400} minWidth={300} minHeight={260}>
46	<div class="profile-content">
47		{#if profile}
48			<div class="identity">
49				<img class="buddy-icon" src="/aim/running-man-48.png" alt="" width="48" height="48" />
50				<div>
51					<strong>{profile.nickname}</strong>
52					<div class="status">
53						<img
54							src={`/aim/status-${profile.status}.svg`}
55							alt=""
56							width="16"
57							height="16"
58						/>{STATUS_LABELS_FR[profile.status]}
59					</div>
60					{#if profile.createdAt}<small
61							>Membre depuis {formatFrenchDateTime(new Date(profile.createdAt))}</small
62						>{/if}
63				</div>
64			</div>
65			{#if profile.awayMessage}<div class="away-message">{profile.awayMessage}</div>{/if}
66			<label for="profile-text">Profil</label>
67			{#if own}
68				<textarea
69					id="profile-text"
70					class="sunken-panel"
71					maxlength="1000"
72					bind:value={text}
73					oninput={() => (saved = false)}
74				></textarea>
75				<small>{text.length}/1000</small>
76			{:else}
77				<div id="profile-text" class="sunken-panel profile-text">
78					{profile.profile ?? 'Aucun profil pour le moment.'}
79				</div>
80			{/if}
81		{:else if profile === null}
82			<p>Profil introuvable : {nickname}</p>
83		{:else}
84			<p>Chargement du profil…</p>
85		{/if}
86		<div class="actions">
87			{#if error}<span role="alert">{error}</span>{/if}
88			{#if saved}<span role="status">Enregistré ✓</span>{/if}
89			{#if own && profile}<button disabled={saving || text.length > 1000} onclick={save}
90					>Enregistrer</button
91				>{/if}
92			<button onclick={() => desktop.close(id)}>Fermer</button>
93		</div>
94	</div>
95</XpWindow>
96
97<style>
98	.profile-content {
99		display: flex;
100		flex-direction: column;
101		gap: 6px;
102		height: 100%;
103		overflow: auto;
104		font:
105			11px Tahoma,
106			'Pixelated MS Sans Serif',
107			sans-serif;
108	}
109	.identity {
110		display: flex;
111		align-items: center;
112		gap: 10px;
113	}
114	.buddy-icon {
115		flex: 0 0 48px;
116	}
117	.status {
118		display: flex;
119		align-items: center;
120		gap: 4px;
121	}
122	.away-message {
123		border: 1px solid #d0c7a0;
124		background: #fff9dc;
125		padding: 6px;
126		white-space: pre-wrap;
127	}
128	.sunken-panel {
129		background: white;
130		padding: 5px;
131		box-sizing: border-box;
132		width: 100%;
133		min-height: 65px;
134		font: inherit;
135	}
136	.profile-text {
137		flex: 1;
138		overflow: auto;
139		white-space: pre-wrap;
140		overflow-wrap: anywhere;
141	}
142	textarea {
143		resize: vertical;
144	}
145	.actions {
146		margin-top: auto;
147		display: flex;
148		justify-content: flex-end;
149		align-items: center;
150		gap: 6px;
151	}
152	.actions span[role='alert'] {
153		color: #a00;
154	}
155</style>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close