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