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 <section class="create-section">
95 <button onclick={handleCreate} disabled={creating}>
96 {creating ? $_('inviteCodes.creating') : $_('inviteCodes.createNew')}
97 </button>
98 </section>
99 <section class="list-section">
100 <h2>{$_('inviteCodes.yourCodes')}</h2>
101 {#if loading}
102 <p class="empty">{$_('common.loading')}</p>
103 {:else if codes.length === 0}
104 <p class="empty">{$_('inviteCodes.noCodes')}</p>
105 {:else}
106 <ul class="code-list">
107 {#each codes as code}
108 <li class:disabled={code.disabled} class:used={code.uses.length > 0 && code.available === 0}>
109 <div class="code-main">
110 <code>{code.code}</code>
111 <button class="copy-small" onclick={() => copyCode(code.code)} title={$_('inviteCodes.copy')}>
112 {$_('inviteCodes.copy')}
113 </button>
114 </div>
115 <div class="code-meta">
116 <span class="date">{$_('inviteCodes.createdOn', { values: { date: formatDate(code.createdAt) } })}</span>
117 {#if code.disabled}
118 <span class="status disabled">{$_('inviteCodes.disabled')}</span>
119 {:else if code.uses.length > 0}
120 <span class="status used">{$_('inviteCodes.used', { values: { handle: code.uses[0].usedBy.split(':').pop() } })}</span>
121 {:else}
122 <span class="status available">{$_('inviteCodes.available')}</span>
123 {/if}
124 </div>
125 </li>
126 {/each}
127 </ul>
128 {/if}
129 </section>
130</div>
131<style>
132 .page {
133 max-width: var(--width-lg);
134 margin: 0 auto;
135 padding: var(--space-7);
136 }
137
138 header {
139 margin-bottom: var(--space-4);
140 }
141
142 .back {
143 color: var(--text-secondary);
144 text-decoration: none;
145 font-size: var(--text-sm);
146 }
147
148 .back:hover {
149 color: var(--accent);
150 }
151
152 h1 {
153 margin: var(--space-2) 0 0 0;
154 }
155
156 .description {
157 color: var(--text-secondary);
158 margin-bottom: var(--space-7);
159 }
160
161 .error {
162 padding: var(--space-3);
163 background: var(--error-bg);
164 border: 1px solid var(--error-border);
165 border-radius: var(--radius-md);
166 color: var(--error-text);
167 margin-bottom: var(--space-4);
168 }
169
170 .created-code {
171 padding: var(--space-6);
172 background: var(--success-bg);
173 border: 1px solid var(--success-border);
174 border-radius: var(--radius-xl);
175 margin-bottom: var(--space-7);
176 }
177
178 .created-code h3 {
179 margin: 0 0 var(--space-4) 0;
180 color: var(--success-text);
181 }
182
183 .code-display {
184 display: flex;
185 align-items: center;
186 gap: var(--space-4);
187 background: var(--bg-card);
188 padding: var(--space-4);
189 border-radius: var(--radius-md);
190 margin-bottom: var(--space-4);
191 }
192
193 .code-display code {
194 font-size: var(--text-lg);
195 font-family: ui-monospace, monospace;
196 flex: 1;
197 }
198
199 .copy {
200 padding: var(--space-2) var(--space-4);
201 background: var(--accent);
202 color: var(--text-inverse);
203 border: none;
204 border-radius: var(--radius-md);
205 cursor: pointer;
206 }
207
208 .copy:hover {
209 background: var(--accent-hover);
210 }
211
212 .create-section {
213 margin-bottom: var(--space-7);
214 }
215
216 section h2 {
217 font-size: var(--text-lg);
218 margin: 0 0 var(--space-4) 0;
219 }
220
221 .code-list {
222 list-style: none;
223 padding: 0;
224 margin: 0;
225 }
226
227 .code-list li {
228 padding: var(--space-4);
229 border: 1px solid var(--border-color);
230 border-radius: var(--radius-md);
231 margin-bottom: var(--space-2);
232 background: var(--bg-card);
233 }
234
235 .code-list li.disabled {
236 opacity: 0.6;
237 }
238
239 .code-list li.used {
240 background: var(--bg-secondary);
241 }
242
243 .code-main {
244 display: flex;
245 align-items: center;
246 gap: var(--space-2);
247 margin-bottom: var(--space-2);
248 }
249
250 .code-main code {
251 font-family: ui-monospace, monospace;
252 font-size: var(--text-sm);
253 }
254
255 .copy-small {
256 padding: var(--space-1) var(--space-2);
257 background: var(--bg-secondary);
258 border: 1px solid var(--border-color);
259 border-radius: var(--radius-md);
260 font-size: var(--text-xs);
261 cursor: pointer;
262 color: var(--text-primary);
263 }
264
265 .copy-small:hover {
266 background: var(--bg-input-disabled);
267 }
268
269 .code-meta {
270 display: flex;
271 gap: var(--space-4);
272 font-size: var(--text-sm);
273 }
274
275 .date {
276 color: var(--text-secondary);
277 }
278
279 .status {
280 padding: var(--space-1) var(--space-2);
281 border-radius: var(--radius-md);
282 font-size: var(--text-xs);
283 }
284
285 .status.available {
286 background: var(--success-bg);
287 color: var(--success-text);
288 }
289
290 .status.used {
291 background: var(--bg-secondary);
292 color: var(--text-secondary);
293 }
294
295 .status.disabled {
296 background: var(--error-bg);
297 color: var(--error-text);
298 }
299
300 .empty {
301 color: var(--text-secondary);
302 text-align: center;
303 padding: var(--space-7);
304 }
305</style>