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, ApiError } from '../lib/api' 5 6 const auth = getAuthState() 7 8 let loading = $state(true) 9 let saving = $state(false) 10 let error = $state<string | null>(null) 11 let success = $state<string | null>(null) 12 13 let preferredChannel = $state('email') 14 let email = $state('') 15 let discordId = $state('') 16 let discordVerified = $state(false) 17 let telegramUsername = $state('') 18 let telegramVerified = $state(false) 19 let signalNumber = $state('') 20 let signalVerified = $state(false) 21 22 $effect(() => { 23 if (!auth.loading && !auth.session) { 24 navigate('/login') 25 } 26 }) 27 28 $effect(() => { 29 if (auth.session) { 30 loadPrefs() 31 } 32 }) 33 34 async function loadPrefs() { 35 if (!auth.session) return 36 loading = true 37 error = null 38 39 try { 40 const prefs = await api.getNotificationPrefs(auth.session.accessJwt) 41 preferredChannel = prefs.preferredChannel 42 email = prefs.email 43 discordId = prefs.discordId ?? '' 44 discordVerified = prefs.discordVerified 45 telegramUsername = prefs.telegramUsername ?? '' 46 telegramVerified = prefs.telegramVerified 47 signalNumber = prefs.signalNumber ?? '' 48 signalVerified = prefs.signalVerified 49 } catch (e) { 50 error = e instanceof ApiError ? e.message : 'Failed to load notification preferences' 51 } finally { 52 loading = false 53 } 54 } 55 56 async function handleSave(e: Event) { 57 e.preventDefault() 58 if (!auth.session) return 59 60 saving = true 61 error = null 62 success = null 63 64 try { 65 await api.updateNotificationPrefs(auth.session.accessJwt, { 66 preferredChannel, 67 discordId: discordId || undefined, 68 telegramUsername: telegramUsername || undefined, 69 signalNumber: signalNumber || undefined, 70 }) 71 success = 'Notification preferences saved' 72 await loadPrefs() 73 } catch (e) { 74 error = e instanceof ApiError ? e.message : 'Failed to save preferences' 75 } finally { 76 saving = false 77 } 78 } 79 80 const channels = [ 81 { id: 'email', name: 'Email', description: 'Receive notifications via email' }, 82 { id: 'discord', name: 'Discord', description: 'Receive notifications via Discord DM' }, 83 { id: 'telegram', name: 'Telegram', description: 'Receive notifications via Telegram' }, 84 { id: 'signal', name: 'Signal', description: 'Receive notifications via Signal' }, 85 ] 86 87 function canSelectChannel(channelId: string): boolean { 88 if (channelId === 'email') return true 89 if (channelId === 'discord') return !!discordId 90 if (channelId === 'telegram') return !!telegramUsername 91 if (channelId === 'signal') return !!signalNumber 92 return false 93 } 94</script> 95 96<div class="page"> 97 <header> 98 <a href="#/dashboard" class="back">&larr; Dashboard</a> 99 <h1>Notification Preferences</h1> 100 </header> 101 102 <p class="description"> 103 Choose how you want to receive important notifications like password resets, 104 security alerts, and account updates. 105 </p> 106 107 {#if loading} 108 <p class="loading">Loading...</p> 109 {:else} 110 {#if error} 111 <div class="message error">{error}</div> 112 {/if} 113 114 {#if success} 115 <div class="message success">{success}</div> 116 {/if} 117 118 <form onsubmit={handleSave}> 119 <section> 120 <h2>Preferred Channel</h2> 121 <p class="section-description"> 122 Select your preferred way to receive notifications. You must configure a channel before you can select it. 123 </p> 124 125 <div class="channel-options"> 126 {#each channels as channel} 127 <label class="channel-option" class:disabled={!canSelectChannel(channel.id)}> 128 <input 129 type="radio" 130 name="preferredChannel" 131 value={channel.id} 132 bind:group={preferredChannel} 133 disabled={!canSelectChannel(channel.id) || saving} 134 /> 135 <div class="channel-info"> 136 <span class="channel-name">{channel.name}</span> 137 <span class="channel-description">{channel.description}</span> 138 {#if channel.id !== 'email' && !canSelectChannel(channel.id)} 139 <span class="channel-hint">Configure below to enable</span> 140 {/if} 141 </div> 142 </label> 143 {/each} 144 </div> 145 </section> 146 147 <section> 148 <h2>Channel Configuration</h2> 149 150 <div class="channel-config"> 151 <div class="config-item"> 152 <label for="email">Email</label> 153 <div class="config-input"> 154 <input 155 id="email" 156 type="email" 157 value={email} 158 disabled 159 class="readonly" 160 /> 161 <span class="status verified">Primary</span> 162 </div> 163 <p class="config-hint">Your email is managed in Account Settings</p> 164 </div> 165 166 <div class="config-item"> 167 <label for="discord">Discord User ID</label> 168 <div class="config-input"> 169 <input 170 id="discord" 171 type="text" 172 bind:value={discordId} 173 placeholder="e.g., 123456789012345678" 174 disabled={saving} 175 /> 176 {#if discordId} 177 {#if discordVerified} 178 <span class="status verified">Verified</span> 179 {:else} 180 <span class="status unverified">Not verified</span> 181 {/if} 182 {/if} 183 </div> 184 <p class="config-hint">Your Discord user ID (not username). Enable Developer Mode in Discord to copy it.</p> 185 </div> 186 187 <div class="config-item"> 188 <label for="telegram">Telegram Username</label> 189 <div class="config-input"> 190 <input 191 id="telegram" 192 type="text" 193 bind:value={telegramUsername} 194 placeholder="e.g., username" 195 disabled={saving} 196 /> 197 {#if telegramUsername} 198 {#if telegramVerified} 199 <span class="status verified">Verified</span> 200 {:else} 201 <span class="status unverified">Not verified</span> 202 {/if} 203 {/if} 204 </div> 205 <p class="config-hint">Your Telegram username without the @ symbol</p> 206 </div> 207 208 <div class="config-item"> 209 <label for="signal">Signal Phone Number</label> 210 <div class="config-input"> 211 <input 212 id="signal" 213 type="tel" 214 bind:value={signalNumber} 215 placeholder="e.g., +1234567890" 216 disabled={saving} 217 /> 218 {#if signalNumber} 219 {#if signalVerified} 220 <span class="status verified">Verified</span> 221 {:else} 222 <span class="status unverified">Not verified</span> 223 {/if} 224 {/if} 225 </div> 226 <p class="config-hint">Your Signal phone number with country code</p> 227 </div> 228 </div> 229 </section> 230 231 <div class="actions"> 232 <button type="submit" disabled={saving}> 233 {saving ? 'Saving...' : 'Save Preferences'} 234 </button> 235 </div> 236 </form> 237 {/if} 238</div> 239 240<style> 241 .page { 242 max-width: 600px; 243 margin: 0 auto; 244 padding: 2rem; 245 } 246 247 header { 248 margin-bottom: 1rem; 249 } 250 251 .back { 252 color: var(--text-secondary); 253 text-decoration: none; 254 font-size: 0.875rem; 255 } 256 257 .back:hover { 258 color: var(--accent); 259 } 260 261 h1 { 262 margin: 0.5rem 0 0 0; 263 } 264 265 .description { 266 color: var(--text-secondary); 267 margin-bottom: 2rem; 268 } 269 270 .loading { 271 text-align: center; 272 color: var(--text-secondary); 273 padding: 2rem; 274 } 275 276 .message { 277 padding: 0.75rem; 278 border-radius: 4px; 279 margin-bottom: 1rem; 280 } 281 282 .message.error { 283 background: var(--error-bg); 284 border: 1px solid var(--error-border); 285 color: var(--error-text); 286 } 287 288 .message.success { 289 background: var(--success-bg); 290 border: 1px solid var(--success-border); 291 color: var(--success-text); 292 } 293 294 section { 295 background: var(--bg-secondary); 296 padding: 1.5rem; 297 border-radius: 8px; 298 margin-bottom: 1.5rem; 299 } 300 301 section h2 { 302 margin: 0 0 0.5rem 0; 303 font-size: 1.125rem; 304 } 305 306 .section-description { 307 color: var(--text-secondary); 308 font-size: 0.875rem; 309 margin: 0 0 1rem 0; 310 } 311 312 .channel-options { 313 display: flex; 314 flex-direction: column; 315 gap: 0.5rem; 316 } 317 318 .channel-option { 319 display: flex; 320 align-items: flex-start; 321 gap: 0.75rem; 322 padding: 0.75rem; 323 background: var(--bg-card); 324 border: 1px solid var(--border-color); 325 border-radius: 4px; 326 cursor: pointer; 327 transition: border-color 0.15s; 328 } 329 330 .channel-option:hover:not(.disabled) { 331 border-color: var(--accent); 332 } 333 334 .channel-option.disabled { 335 opacity: 0.6; 336 cursor: not-allowed; 337 } 338 339 .channel-option input { 340 margin-top: 0.25rem; 341 } 342 343 .channel-info { 344 display: flex; 345 flex-direction: column; 346 gap: 0.125rem; 347 } 348 349 .channel-name { 350 font-weight: 500; 351 } 352 353 .channel-description { 354 font-size: 0.875rem; 355 color: var(--text-secondary); 356 } 357 358 .channel-hint { 359 font-size: 0.75rem; 360 color: var(--text-muted); 361 font-style: italic; 362 } 363 364 .channel-config { 365 display: flex; 366 flex-direction: column; 367 gap: 1.25rem; 368 } 369 370 .config-item { 371 display: flex; 372 flex-direction: column; 373 gap: 0.25rem; 374 } 375 376 .config-item label { 377 font-size: 0.875rem; 378 font-weight: 500; 379 } 380 381 .config-input { 382 display: flex; 383 align-items: center; 384 gap: 0.5rem; 385 } 386 387 .config-input input { 388 flex: 1; 389 padding: 0.75rem; 390 border: 1px solid var(--border-color-light); 391 border-radius: 4px; 392 font-size: 1rem; 393 background: var(--bg-input); 394 color: var(--text-primary); 395 } 396 397 .config-input input:focus { 398 outline: none; 399 border-color: var(--accent); 400 } 401 402 .config-input input.readonly { 403 background: var(--bg-input-disabled); 404 color: var(--text-secondary); 405 } 406 407 .status { 408 padding: 0.25rem 0.5rem; 409 border-radius: 4px; 410 font-size: 0.75rem; 411 white-space: nowrap; 412 } 413 414 .status.verified { 415 background: var(--success-bg); 416 color: var(--success-text); 417 } 418 419 .status.unverified { 420 background: var(--warning-bg); 421 color: var(--warning-text); 422 } 423 424 .config-hint { 425 font-size: 0.75rem; 426 color: var(--text-secondary); 427 margin: 0; 428 } 429 430 .actions { 431 display: flex; 432 justify-content: flex-end; 433 } 434 435 .actions button { 436 padding: 0.75rem 2rem; 437 background: var(--accent); 438 color: white; 439 border: none; 440 border-radius: 4px; 441 font-size: 1rem; 442 cursor: pointer; 443 } 444 445 .actions button:hover:not(:disabled) { 446 background: var(--accent-hover); 447 } 448 449 .actions button:disabled { 450 opacity: 0.6; 451 cursor: not-allowed; 452 } 453</style>