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