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 <section class="create-section"> 95 <button onclick={handleCreate} disabled={creating}> 96 {creating ? $_('inviteCodes.creating') : $_('inviteCodes.createNew')} 97 </button> 98 </section> 99 <section class="list-section"> 100 <h2>{$_('inviteCodes.yourCodes')}</h2> 101 {#if loading} 102 <p class="empty">{$_('common.loading')}</p> 103 {:else if codes.length === 0} 104 <p class="empty">{$_('inviteCodes.noCodes')}</p> 105 {:else} 106 <ul class="code-list"> 107 {#each codes as code} 108 <li class:disabled={code.disabled} class:used={code.uses.length > 0 && code.available === 0}> 109 <div class="code-main"> 110 <code>{code.code}</code> 111 <button class="copy-small" onclick={() => copyCode(code.code)} title={$_('inviteCodes.copy')}> 112 {$_('inviteCodes.copy')} 113 </button> 114 </div> 115 <div class="code-meta"> 116 <span class="date">{$_('inviteCodes.createdOn', { values: { date: formatDate(code.createdAt) } })}</span> 117 {#if code.disabled} 118 <span class="status disabled">{$_('inviteCodes.disabled')}</span> 119 {:else if code.uses.length > 0} 120 <span class="status used">{$_('inviteCodes.used', { values: { handle: code.uses[0].usedBy.split(':').pop() } })}</span> 121 {:else} 122 <span class="status available">{$_('inviteCodes.available')}</span> 123 {/if} 124 </div> 125 </li> 126 {/each} 127 </ul> 128 {/if} 129 </section> 130</div> 131<style> 132 .page { 133 max-width: var(--width-lg); 134 margin: 0 auto; 135 padding: var(--space-7); 136 } 137 138 header { 139 margin-bottom: var(--space-4); 140 } 141 142 .back { 143 color: var(--text-secondary); 144 text-decoration: none; 145 font-size: var(--text-sm); 146 } 147 148 .back:hover { 149 color: var(--accent); 150 } 151 152 h1 { 153 margin: var(--space-2) 0 0 0; 154 } 155 156 .description { 157 color: var(--text-secondary); 158 margin-bottom: var(--space-7); 159 } 160 161 .error { 162 padding: var(--space-3); 163 background: var(--error-bg); 164 border: 1px solid var(--error-border); 165 border-radius: var(--radius-md); 166 color: var(--error-text); 167 margin-bottom: var(--space-4); 168 } 169 170 .created-code { 171 padding: var(--space-6); 172 background: var(--success-bg); 173 border: 1px solid var(--success-border); 174 border-radius: var(--radius-xl); 175 margin-bottom: var(--space-7); 176 } 177 178 .created-code h3 { 179 margin: 0 0 var(--space-4) 0; 180 color: var(--success-text); 181 } 182 183 .code-display { 184 display: flex; 185 align-items: center; 186 gap: var(--space-4); 187 background: var(--bg-card); 188 padding: var(--space-4); 189 border-radius: var(--radius-md); 190 margin-bottom: var(--space-4); 191 } 192 193 .code-display code { 194 font-size: var(--text-lg); 195 font-family: ui-monospace, monospace; 196 flex: 1; 197 } 198 199 .copy { 200 padding: var(--space-2) var(--space-4); 201 background: var(--accent); 202 color: var(--text-inverse); 203 border: none; 204 border-radius: var(--radius-md); 205 cursor: pointer; 206 } 207 208 .copy:hover { 209 background: var(--accent-hover); 210 } 211 212 .create-section { 213 margin-bottom: var(--space-7); 214 } 215 216 section h2 { 217 font-size: var(--text-lg); 218 margin: 0 0 var(--space-4) 0; 219 } 220 221 .code-list { 222 list-style: none; 223 padding: 0; 224 margin: 0; 225 } 226 227 .code-list li { 228 padding: var(--space-4); 229 border: 1px solid var(--border-color); 230 border-radius: var(--radius-md); 231 margin-bottom: var(--space-2); 232 background: var(--bg-card); 233 } 234 235 .code-list li.disabled { 236 opacity: 0.6; 237 } 238 239 .code-list li.used { 240 background: var(--bg-secondary); 241 } 242 243 .code-main { 244 display: flex; 245 align-items: center; 246 gap: var(--space-2); 247 margin-bottom: var(--space-2); 248 } 249 250 .code-main code { 251 font-family: ui-monospace, monospace; 252 font-size: var(--text-sm); 253 } 254 255 .copy-small { 256 padding: var(--space-1) var(--space-2); 257 background: var(--bg-secondary); 258 border: 1px solid var(--border-color); 259 border-radius: var(--radius-md); 260 font-size: var(--text-xs); 261 cursor: pointer; 262 color: var(--text-primary); 263 } 264 265 .copy-small:hover { 266 background: var(--bg-input-disabled); 267 } 268 269 .code-meta { 270 display: flex; 271 gap: var(--space-4); 272 font-size: var(--text-sm); 273 } 274 275 .date { 276 color: var(--text-secondary); 277 } 278 279 .status { 280 padding: var(--space-1) var(--space-2); 281 border-radius: var(--radius-md); 282 font-size: var(--text-xs); 283 } 284 285 .status.available { 286 background: var(--success-bg); 287 color: var(--success-text); 288 } 289 290 .status.used { 291 background: var(--bg-secondary); 292 color: var(--text-secondary); 293 } 294 295 .status.disabled { 296 background: var(--error-bg); 297 color: var(--error-text); 298 } 299 300 .empty { 301 color: var(--text-secondary); 302 text-align: center; 303 padding: var(--space-7); 304 } 305</style>