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