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