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">&larr; 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>