this repo has no description
1<script lang="ts">
2 import { getAuthState } from '../lib/auth.svelte'
3 import { navigate } from '../lib/router.svelte'
4 import { api, type InviteCode, ApiError } from '../lib/api'
5 import { _ } from '../lib/i18n'
6 import { formatDate } from '../lib/date'
7 import { onMount } from 'svelte'
8
9 const auth = getAuthState()
10 let codes = $state<InviteCode[]>([])
11 let loading = $state(true)
12 let error = $state<string | null>(null)
13 let creating = $state(false)
14 let createdCode = $state<string | null>(null)
15 let createdCodeCopied = $state(false)
16 let copiedCode = $state<string | null>(null)
17 let inviteCodesEnabled = $state<boolean | null>(null)
18
19 onMount(async () => {
20 try {
21 const serverInfo = await api.describeServer()
22 inviteCodesEnabled = serverInfo.inviteCodeRequired
23 if (!serverInfo.inviteCodeRequired) {
24 navigate('/dashboard')
25 }
26 } catch {
27 navigate('/dashboard')
28 }
29 })
30
31 $effect(() => {
32 if (!auth.loading && !auth.session) {
33 navigate('/login')
34 }
35 })
36 $effect(() => {
37 if (auth.session && inviteCodesEnabled) {
38 loadCodes()
39 }
40 })
41 async function loadCodes() {
42 if (!auth.session) return
43 loading = true
44 error = null
45 try {
46 const result = await api.getAccountInviteCodes(auth.session.accessJwt)
47 codes = result.codes
48 } catch (e) {
49 error = e instanceof ApiError ? e.message : 'Failed to load invite codes'
50 } finally {
51 loading = false
52 }
53 }
54 async function handleCreate() {
55 if (!auth.session) return
56 creating = true
57 error = null
58 try {
59 const result = await api.createInviteCode(auth.session.accessJwt, 1)
60 createdCode = result.code
61 await loadCodes()
62 } catch (e) {
63 error = e instanceof ApiError ? e.message : 'Failed to create invite code'
64 } finally {
65 creating = false
66 }
67 }
68 function dismissCreated() {
69 createdCode = null
70 createdCodeCopied = false
71 }
72 function copyCreatedCode() {
73 if (createdCode) {
74 navigator.clipboard.writeText(createdCode)
75 createdCodeCopied = true
76 }
77 }
78 function copyCode(code: string) {
79 navigator.clipboard.writeText(code)
80 copiedCode = code
81 setTimeout(() => {
82 if (copiedCode === code) {
83 copiedCode = null
84 }
85 }, 2000)
86 }
87</script>
88<div class="page">
89 <header>
90 <a href="#/dashboard" class="back">{$_('common.backToDashboard')}</a>
91 <h1>{$_('inviteCodes.title')}</h1>
92 </header>
93 <p class="description">
94 {$_('inviteCodes.description')}
95 </p>
96 {#if error}
97 <div class="error">{error}</div>
98 {/if}
99 {#if createdCode}
100 <div class="created-code">
101 <h3>{$_('inviteCodes.created')}</h3>
102 <div class="code-display">
103 <code>{createdCode}</code>
104 <button class="copy" onclick={copyCreatedCode}>
105 {createdCodeCopied ? $_('common.copied') : $_('common.copyToClipboard')}
106 </button>
107 </div>
108 <button onclick={dismissCreated}>{$_('common.done')}</button>
109 </div>
110 {/if}
111 {#if auth.session?.isAdmin}
112 <section class="create-section">
113 <button onclick={handleCreate} disabled={creating}>
114 {creating ? $_('inviteCodes.creating') : $_('inviteCodes.createNew')}
115 </button>
116 </section>
117 {/if}
118 <section class="list-section">
119 <h2>{$_('inviteCodes.yourCodes')}</h2>
120 {#if loading}
121 <p class="empty">{$_('common.loading')}</p>
122 {:else if codes.length === 0}
123 <p class="empty">{$_('inviteCodes.noCodes')}</p>
124 {:else}
125 <ul class="code-list">
126 {#each codes as code}
127 <li class:disabled={code.disabled} class:used={code.uses.length > 0 && code.available === 0}>
128 <div class="code-main">
129 <code>{code.code}</code>
130 <button
131 class="copy-small"
132 onclick={() => copyCode(code.code)}
133 title={copiedCode === code.code ? $_('common.copied') : $_('inviteCodes.copy')}
134 >
135 {copiedCode === code.code ? $_('common.copied') : $_('inviteCodes.copy')}
136 </button>
137 </div>
138 <div class="code-meta">
139 <span class="date">{$_('inviteCodes.createdOn', { values: { date: formatDate(code.createdAt) } })}</span>
140 {#if code.disabled}
141 <span class="status disabled">{$_('inviteCodes.disabled')}</span>
142 {:else if code.uses.length > 0}
143 <span class="status used">{$_('inviteCodes.used', { values: { handle: code.uses[0].usedByHandle || code.uses[0].usedBy.split(':').pop() } })}</span>
144 {:else}
145 <span class="status available">{$_('inviteCodes.available')}</span>
146 {/if}
147 </div>
148 </li>
149 {/each}
150 </ul>
151 {/if}
152 </section>
153</div>
154<style>
155 .page {
156 max-width: var(--width-lg);
157 margin: 0 auto;
158 padding: var(--space-7);
159 }
160
161 header {
162 margin-bottom: var(--space-4);
163 }
164
165 .back {
166 color: var(--text-secondary);
167 text-decoration: none;
168 font-size: var(--text-sm);
169 }
170
171 .back:hover {
172 color: var(--accent);
173 }
174
175 h1 {
176 margin: var(--space-2) 0 0 0;
177 }
178
179 .description {
180 color: var(--text-secondary);
181 margin-bottom: var(--space-7);
182 }
183
184 .error {
185 padding: var(--space-3);
186 background: var(--error-bg);
187 border: 1px solid var(--error-border);
188 border-radius: var(--radius-md);
189 color: var(--error-text);
190 margin-bottom: var(--space-4);
191 }
192
193 .created-code {
194 padding: var(--space-6);
195 background: var(--success-bg);
196 border: 1px solid var(--success-border);
197 border-radius: var(--radius-xl);
198 margin-bottom: var(--space-7);
199 }
200
201 .created-code h3 {
202 margin: 0 0 var(--space-4) 0;
203 color: var(--success-text);
204 }
205
206 .code-display {
207 display: flex;
208 align-items: center;
209 gap: var(--space-4);
210 background: var(--bg-card);
211 padding: var(--space-4);
212 border-radius: var(--radius-md);
213 margin-bottom: var(--space-4);
214 }
215
216 .code-display code {
217 font-size: var(--text-lg);
218 font-family: ui-monospace, monospace;
219 flex: 1;
220 }
221
222 .copy {
223 padding: var(--space-2) var(--space-4);
224 background: var(--accent);
225 color: var(--text-inverse);
226 border: none;
227 border-radius: var(--radius-md);
228 cursor: pointer;
229 }
230
231 .copy:hover {
232 background: var(--accent-hover);
233 }
234
235 .create-section {
236 margin-bottom: var(--space-7);
237 }
238
239 section h2 {
240 font-size: var(--text-lg);
241 margin: 0 0 var(--space-4) 0;
242 }
243
244 .code-list {
245 list-style: none;
246 padding: 0;
247 margin: 0;
248 }
249
250 .code-list li {
251 padding: var(--space-4);
252 border: 1px solid var(--border-color);
253 border-radius: var(--radius-md);
254 margin-bottom: var(--space-2);
255 background: var(--bg-card);
256 }
257
258 .code-list li.disabled {
259 opacity: 0.6;
260 }
261
262 .code-list li.used {
263 background: var(--bg-secondary);
264 }
265
266 .code-main {
267 display: flex;
268 align-items: center;
269 gap: var(--space-2);
270 margin-bottom: var(--space-2);
271 }
272
273 .code-main code {
274 font-family: ui-monospace, monospace;
275 font-size: var(--text-sm);
276 }
277
278 .copy-small {
279 padding: var(--space-1) var(--space-2);
280 background: var(--bg-secondary);
281 border: 1px solid var(--border-color);
282 border-radius: var(--radius-md);
283 font-size: var(--text-xs);
284 cursor: pointer;
285 color: var(--text-primary);
286 }
287
288 .copy-small:hover {
289 background: var(--bg-input-disabled);
290 }
291
292 .code-meta {
293 display: flex;
294 gap: var(--space-4);
295 font-size: var(--text-sm);
296 }
297
298 .date {
299 color: var(--text-secondary);
300 }
301
302 .status {
303 padding: var(--space-1) var(--space-2);
304 border-radius: var(--radius-md);
305 font-size: var(--text-xs);
306 }
307
308 .status.available {
309 background: var(--success-bg);
310 color: var(--success-text);
311 }
312
313 .status.used {
314 background: var(--bg-secondary);
315 color: var(--text-secondary);
316 }
317
318 .status.disabled {
319 background: var(--error-bg);
320 color: var(--error-text);
321 }
322
323 .empty {
324 color: var(--text-secondary);
325 text-align: center;
326 padding: var(--space-7);
327 }
328</style>