Our Personal Data Server from scratch!
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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