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