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