Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit a6111a6

Browse files
committed
Migrations page layout
1 parent 16ca4e3 commit a6111a6

3 files changed

Lines changed: 440 additions & 209 deletions

File tree

src/routes/(protected)/chat-rooms/[chat_room_id]/+page.svelte

Lines changed: 60 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
const level = $derived(page.url.searchParams.get("level") || "system");
88
const bankId = $derived(page.url.searchParams.get("bank_id") || "");
99
10+
const addParticipantLink = $derived(
11+
chatRoomId
12+
? `/chat-rooms/${encodeURIComponent(chatRoomId)}/add-participant?level=${encodeURIComponent(level)}${bankId ? `&bank_id=${encodeURIComponent(bankId)}` : ""}`
13+
: "",
14+
);
15+
1016
let room = $state<OBPChatRoom | null>(null);
1117
let participants = $state<OBPChatRoomParticipant[]>([]);
1218
let loading = $state(false);
1319
let error = $state<string | null>(null);
1420
let successMessage = $state<string | null>(null);
1521
16-
let showAddForm = $state(false);
17-
let addUserId = $state("");
18-
let addPermissions = $state("can_send_message");
19-
2022
async function fetchRoom() {
2123
if (!chatRoomId) return;
2224
try {
@@ -25,12 +27,17 @@
2527
`/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}${bankParam}`,
2628
);
2729
if (!res.ok) {
28-
const data = await res.json().catch(() => ({}));
29-
throw new Error(data.error || "Failed to fetch chat room");
30+
const data = await res.json();
31+
if (typeof data.message !== "string") {
32+
throw new Error(
33+
`OBP error response missing 'message' field (HTTP ${res.status})`,
34+
);
35+
}
36+
throw new Error(data.message);
3037
}
3138
room = await res.json();
3239
} catch (err) {
33-
error = err instanceof Error ? err.message : "Failed to fetch chat room";
40+
error = err instanceof Error ? err.message : String(err);
3441
room = null;
3542
}
3643
}
@@ -45,80 +52,24 @@
4552
`/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}/participants?level=${level}${bankParam}`,
4653
);
4754
if (!res.ok) {
48-
const data = await res.json().catch(() => ({}));
49-
throw new Error(data.error || "Failed to fetch participants");
55+
const data = await res.json();
56+
if (typeof data.message !== "string") {
57+
throw new Error(
58+
`OBP error response missing 'message' field (HTTP ${res.status})`,
59+
);
60+
}
61+
throw new Error(data.message);
5062
}
5163
const data = await res.json();
52-
participants = data.participants || [];
64+
participants = data.participants;
5365
} catch (err) {
54-
error = err instanceof Error ? err.message : "Failed to fetch participants";
66+
error = err instanceof Error ? err.message : String(err);
5567
participants = [];
5668
} finally {
5769
loading = false;
5870
}
5971
}
6072
61-
async function addParticipant() {
62-
if (!addUserId.trim() || !chatRoomId) return;
63-
error = null;
64-
successMessage = null;
65-
try {
66-
const bankParam = bankId ? `&bank_id=${encodeURIComponent(bankId)}` : "";
67-
const res = await trackedFetch(
68-
`/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}/participants?level=${level}${bankParam}`,
69-
{
70-
method: "POST",
71-
headers: { "Content-Type": "application/json" },
72-
body: JSON.stringify({
73-
user_id: addUserId,
74-
permissions: addPermissions.split(",").map((p) => p.trim()).filter(Boolean),
75-
}),
76-
},
77-
);
78-
if (!res.ok) {
79-
const data = await res.json().catch(() => ({}));
80-
throw new Error(data.error || "Failed to add participant");
81-
}
82-
successMessage = "Participant added.";
83-
addUserId = "";
84-
showAddForm = false;
85-
fetchParticipants();
86-
} catch (err) {
87-
error = err instanceof Error ? err.message : "Failed to add participant";
88-
}
89-
}
90-
91-
async function refreshJoiningKey() {
92-
if (!chatRoomId) return;
93-
error = null;
94-
successMessage = null;
95-
try {
96-
const endpoint =
97-
level === "bank" && bankId
98-
? `/proxy/obp/v6.0.0/banks/${encodeURIComponent(bankId)}/chat-rooms/${encodeURIComponent(chatRoomId)}/joining-key`
99-
: `/proxy/obp/v6.0.0/chat-rooms/${encodeURIComponent(chatRoomId)}/joining-key`;
100-
const res = await trackedFetch(endpoint, { method: "PUT" });
101-
if (!res.ok) {
102-
const data = await res.json().catch(() => ({}));
103-
throw new Error(data.error || "Failed to refresh joining key");
104-
}
105-
successMessage = "Joining key refreshed.";
106-
fetchRoom();
107-
} catch (err) {
108-
error = err instanceof Error ? err.message : "Failed to refresh joining key";
109-
}
110-
}
111-
112-
async function copyJoiningKey() {
113-
if (!room?.joining_key) return;
114-
try {
115-
await navigator.clipboard.writeText(room.joining_key);
116-
successMessage = "Joining key copied.";
117-
} catch {
118-
error = "Could not copy to clipboard.";
119-
}
120-
}
121-
12273
async function removeParticipant(userId: string) {
12374
if (!chatRoomId) return;
12475
error = null;
@@ -130,13 +81,18 @@
13081
{ method: "DELETE" },
13182
);
13283
if (!res.ok) {
133-
const data = await res.json().catch(() => ({}));
134-
throw new Error(data.error || "Failed to remove participant");
84+
const data = await res.json();
85+
if (typeof data.message !== "string") {
86+
throw new Error(
87+
`OBP error response missing 'message' field (HTTP ${res.status})`,
88+
);
89+
}
90+
throw new Error(data.message);
13591
}
13692
successMessage = "Participant removed.";
13793
fetchParticipants();
13894
} catch (err) {
139-
error = err instanceof Error ? err.message : "Failed to remove participant";
95+
error = err instanceof Error ? err.message : String(err);
14096
}
14197
}
14298
@@ -171,56 +127,23 @@
171127
</div>
172128

173129
<div class="flex items-center justify-between mb-4">
174-
<div>
175-
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">
176-
Chat Room Participants
177-
</h1>
178-
<p class="text-sm text-gray-600 dark:text-gray-400 font-mono mt-1">{chatRoomId}</p>
179-
<p class="text-xs text-gray-500 dark:text-gray-500 mt-0.5">
180-
Level: {level}{bankId ? ` | Bank: ${bankId}` : ""}
181-
</p>
182-
</div>
183-
<div class="flex items-center gap-3">
130+
<p class="text-sm text-gray-600 dark:text-gray-400">
131+
<span class="font-mono">{chatRoomId}</span> &middot; Level: {level}{bankId ? ` · Bank: ${bankId}` : ""}
184132
{#if participants.length > 0}
185-
<span class="text-sm text-gray-600 dark:text-gray-400">
186-
Participants: {participants.length}
187-
</span>
188-
{/if}
189-
{#if !room?.is_open_room}
190-
<button
191-
onclick={() => (showAddForm = !showAddForm)}
192-
class="inline-flex items-center rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
193-
data-testid="add-participant-btn"
194-
>
195-
{showAddForm ? "Cancel" : "Add Participant"}
196-
</button>
133+
&middot; Participants: {participants.length}
197134
{/if}
198-
</div>
135+
</p>
136+
{#if !room?.is_open_room}
137+
<a
138+
href={addParticipantLink}
139+
class="inline-flex items-center rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
140+
data-testid="add-participant-btn"
141+
>
142+
Add Participant
143+
</a>
144+
{/if}
199145
</div>
200146

201-
{#if room && !room.is_open_room && room.joining_key}
202-
<div class="mb-4 rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-700 dark:bg-gray-800" data-testid="joining-key-panel">
203-
<div class="flex flex-wrap items-center gap-3">
204-
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Joining Key:</span>
205-
<code class="flex-1 min-w-0 truncate rounded bg-gray-100 px-2 py-1 font-mono text-sm text-gray-900 dark:bg-gray-900 dark:text-gray-100" data-testid="joining-key-value">{room.joining_key}</code>
206-
<button
207-
onclick={copyJoiningKey}
208-
class="inline-flex items-center rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
209-
data-testid="copy-joining-key-btn"
210-
>
211-
Copy
212-
</button>
213-
<button
214-
onclick={refreshJoiningKey}
215-
class="inline-flex items-center rounded border border-gray-300 bg-white px-2 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
216-
data-testid="refresh-joining-key-btn"
217-
>
218-
Refresh
219-
</button>
220-
</div>
221-
</div>
222-
{/if}
223-
224147
{#if successMessage}
225148
<div class="mb-4 rounded-lg border border-green-200 bg-green-50 p-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200" data-testid="success-message">
226149
{successMessage}
@@ -233,76 +156,39 @@
233156
</div>
234157
{/if}
235158

236-
{#if showAddForm}
237-
<div class="mb-4 rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800" data-testid="add-participant-form">
238-
<h2 class="mb-3 text-lg font-semibold text-gray-900 dark:text-gray-100">Add Participant</h2>
239-
<div class="space-y-3">
240-
<div>
241-
<label for="user_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">User ID</label>
242-
<input
243-
type="text"
244-
id="user_id"
245-
bind:value={addUserId}
246-
class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
247-
data-testid="participant-user-id-input"
248-
/>
249-
</div>
250-
<div>
251-
<label for="permissions" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Permissions (comma-separated)</label>
252-
<input
253-
type="text"
254-
id="permissions"
255-
bind:value={addPermissions}
256-
placeholder="can_send_message, can_delete_message, can_update_room"
257-
class="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
258-
data-testid="participant-permissions-input"
259-
/>
260-
</div>
261-
<button
262-
onclick={addParticipant}
263-
disabled={!addUserId.trim()}
264-
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-600"
265-
data-testid="submit-add-participant"
266-
>
267-
Add
268-
</button>
269-
</div>
270-
</div>
271-
{/if}
272-
273159
{#if loading}
274160
<div class="flex items-center justify-center py-8">
275161
<div class="h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent"></div>
276-
<span class="ml-2 text-gray-600 dark:text-gray-400">Loading...</span>
162+
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Loading...</span>
277163
</div>
278164
{:else if participants.length > 0}
279165
<div class="overflow-x-auto">
280166
<table class="w-full border-collapse" data-testid="participants-table">
281167
<thead>
282168
<tr class="border-b-2 border-gray-200 dark:border-gray-700">
283-
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">User ID</th>
284-
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Permissions</th>
285-
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Joined</th>
286-
<th class="text-left px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Muted</th>
287-
<th class="text-right px-3 py-2 text-xs font-medium text-gray-600 dark:text-gray-400">Actions</th>
169+
<th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">User ID</th>
170+
<th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Permissions</th>
171+
<th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Joined</th>
172+
<th class="text-left px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Muted</th>
173+
<th class="text-right px-3 py-2 text-sm font-medium text-gray-600 dark:text-gray-400">Actions</th>
288174
</tr>
289175
</thead>
290176
<tbody>
291177
{#each participants as participant (participant.participant_id)}
292178
<tr class="border-b border-gray-100 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800/50" data-testid="participant-row-{participant.user_id}">
293-
<td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100">
179+
<td class="px-3 py-2 text-sm text-gray-900 dark:text-gray-100">
294180
<span class="font-medium">{participant.username || participant.user_id}</span>
295181
{#if participant.provider}
296-
<br /><span class="text-gray-500 dark:text-gray-500">{participant.provider}</span>
182+
<br /><span class="text-xs text-gray-500 dark:text-gray-500">{participant.provider}</span>
297183
{/if}
298184
{#if participant.username}
299-
<br /><span class="font-mono text-gray-500 dark:text-gray-500">{participant.user_id}</span>
185+
<br /><span class="text-xs font-mono text-gray-500 dark:text-gray-500">{participant.user_id}</span>
300186
{/if}
301187
{#if participant.consumer_id}
302-
<br /><span class="text-gray-500">Consumer: {participant.consumer_name || participant.consumer_id}</span>
188+
<br /><span class="text-xs text-gray-500">Consumer: {participant.consumer_name || participant.consumer_id}</span>
303189
{/if}
304190
</td>
305-
<td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100">
191+
<td class="px-3 py-2 text-sm text-gray-900 dark:text-gray-100">
306192
<div class="flex flex-wrap gap-1">
307193
{#each participant.permissions as perm}
308194
<span class="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
@@ -311,8 +197,8 @@
311197
{/each}
312198
</div>
313199
</td>
314-
<td class="px-3 py-2 text-xs text-gray-900 dark:text-gray-100">{formatDate(participant.joined_at)}</td>
315-
<td class="px-3 py-2 text-xs">
200+
<td class="px-3 py-2 text-sm text-gray-900 dark:text-gray-100">{formatDate(participant.joined_at)}</td>
201+
<td class="px-3 py-2 text-sm">
316202
<span class="rounded-full px-2 py-0.5 text-xs font-medium {participant.is_muted
317203
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400'
318204
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'}">
@@ -336,14 +222,14 @@
336222
{:else if !error}
337223
<div class="rounded-lg bg-gray-100 p-8 text-center dark:bg-gray-800" data-testid="empty-participants">
338224
{#if room?.is_open_room}
339-
<p class="text-lg font-medium text-gray-700 dark:text-gray-300" data-testid="everyone-label">
225+
<p class="text-sm font-medium text-gray-700 dark:text-gray-300" data-testid="everyone-label">
340226
This room is Open. Everyone can join.
341227
</p>
342228
{:else}
343-
<p class="text-lg font-medium text-gray-700 dark:text-gray-300">
229+
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">
344230
No participants found
345231
</p>
346-
<p class="mt-1 text-gray-600 dark:text-gray-400">
232+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
347233
Add a participant to this chat room.
348234
</p>
349235
{/if}

0 commit comments

Comments
 (0)