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