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