Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql

chore: add editorconfig and format examples html

+753 -684
+9
.editorconfig
··· 1 + root = true 2 + 3 + [*] 4 + indent_style = space 5 + indent_size = 2 6 + end_of_line = lf 7 + charset = utf-8 8 + trim_trailing_whitespace = true 9 + insert_final_newline = true
+6 -1
Makefile
··· 1 - .PHONY: help test build clean run css 1 + .PHONY: help test build clean run css format-examples 2 2 3 3 help: 4 4 @echo "quickslice - Makefile Commands" ··· 7 7 @echo " make test - Run all tests" 8 8 @echo " make build - Build all projects" 9 9 @echo " make clean - Clean build artifacts" 10 + @echo " make format-examples - Format example HTML files" 10 11 @echo "" 11 12 12 13 # Run all tests ··· 29 30 @cd lexicon_graphql && gleam clean 30 31 @cd server && gleam clean 31 32 @echo "Clean complete" 33 + 34 + # Format example HTML files 35 + format-examples: 36 + @prettier --write "examples/**/*.html"
+738 -683
examples/01-statusphere/index.html
··· 1 - <!DOCTYPE html> 1 + <!doctype html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src http://localhost:8080; img-src 'self' https: data:;"> 7 - <title>Statusphere</title> 8 - <style> 9 - /* CSS Reset */ 10 - *, *::before, *::after { 11 - box-sizing: border-box; 12 - } 13 - * { 14 - margin: 0; 15 - } 16 - body { 17 - line-height: 1.5; 18 - -webkit-font-smoothing: antialiased; 19 - } 20 - input, button { 21 - font: inherit; 22 - } 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <meta 7 + http-equiv="Content-Security-Policy" 8 + content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src http://localhost:8080; img-src 'self' https: data:;" 9 + /> 10 + <title>Statusphere</title> 11 + <style> 12 + /* CSS Reset */ 13 + *, 14 + *::before, 15 + *::after { 16 + box-sizing: border-box; 17 + } 18 + * { 19 + margin: 0; 20 + } 21 + body { 22 + line-height: 1.5; 23 + -webkit-font-smoothing: antialiased; 24 + } 25 + input, 26 + button { 27 + font: inherit; 28 + } 23 29 24 - /* CSS Variables */ 25 - :root { 26 - --primary-500: #0078ff; 27 - --primary-400: #339dff; 28 - --primary-600: #0060cc; 29 - --gray-100: #f5f5f5; 30 - --gray-200: #e5e5e5; 31 - --gray-500: #737373; 32 - --gray-700: #404040; 33 - --gray-900: #171717; 34 - --border-color: #e5e5e5; 35 - --error-bg: #fef2f2; 36 - --error-border: #fecaca; 37 - --error-text: #dc2626; 38 - } 30 + /* CSS Variables */ 31 + :root { 32 + --primary-500: #0078ff; 33 + --primary-400: #339dff; 34 + --primary-600: #0060cc; 35 + --gray-100: #f5f5f5; 36 + --gray-200: #e5e5e5; 37 + --gray-500: #737373; 38 + --gray-700: #404040; 39 + --gray-900: #171717; 40 + --border-color: #e5e5e5; 41 + --error-bg: #fef2f2; 42 + --error-border: #fecaca; 43 + --error-text: #dc2626; 44 + } 39 45 40 - /* Layout */ 41 - body { 42 - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 43 - background: var(--gray-100); 44 - color: var(--gray-900); 45 - min-height: 100vh; 46 - padding: 2rem 1rem; 47 - } 46 + /* Layout */ 47 + body { 48 + font-family: 49 + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 50 + background: var(--gray-100); 51 + color: var(--gray-900); 52 + min-height: 100vh; 53 + padding: 2rem 1rem; 54 + } 48 55 49 - #app { 50 - max-width: 600px; 51 - margin: 0 auto; 52 - } 56 + #app { 57 + max-width: 600px; 58 + margin: 0 auto; 59 + } 53 60 54 - /* Header */ 55 - header { 56 - text-align: center; 57 - margin-bottom: 2rem; 58 - } 61 + /* Header */ 62 + header { 63 + text-align: center; 64 + margin-bottom: 2rem; 65 + } 59 66 60 - header h1 { 61 - font-size: 2.5rem; 62 - color: var(--primary-500); 63 - margin-bottom: 0.25rem; 64 - } 67 + header h1 { 68 + font-size: 2.5rem; 69 + color: var(--primary-500); 70 + margin-bottom: 0.25rem; 71 + } 65 72 66 - .tagline { 67 - color: var(--gray-500); 68 - font-size: 1rem; 69 - } 73 + .tagline { 74 + color: var(--gray-500); 75 + font-size: 1rem; 76 + } 70 77 71 - /* Cards */ 72 - .card { 73 - background: white; 74 - border-radius: 0.5rem; 75 - padding: 1.5rem; 76 - margin-bottom: 1rem; 77 - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 78 - } 78 + /* Cards */ 79 + .card { 80 + background: white; 81 + border-radius: 0.5rem; 82 + padding: 1.5rem; 83 + margin-bottom: 1rem; 84 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 85 + } 79 86 80 - /* Auth Section */ 81 - .login-form { 82 - display: flex; 83 - flex-direction: column; 84 - gap: 1rem; 85 - } 87 + /* Auth Section */ 88 + .login-form { 89 + display: flex; 90 + flex-direction: column; 91 + gap: 1rem; 92 + } 86 93 87 - .form-group { 88 - display: flex; 89 - flex-direction: column; 90 - gap: 0.25rem; 91 - } 94 + .form-group { 95 + display: flex; 96 + flex-direction: column; 97 + gap: 0.25rem; 98 + } 92 99 93 - .form-group label { 94 - font-size: 0.875rem; 95 - font-weight: 500; 96 - color: var(--gray-700); 97 - } 100 + .form-group label { 101 + font-size: 0.875rem; 102 + font-weight: 500; 103 + color: var(--gray-700); 104 + } 98 105 99 - .form-group input { 100 - padding: 0.75rem; 101 - border: 1px solid var(--border-color); 102 - border-radius: 0.375rem; 103 - font-size: 1rem; 104 - } 106 + .form-group input { 107 + padding: 0.75rem; 108 + border: 1px solid var(--border-color); 109 + border-radius: 0.375rem; 110 + font-size: 1rem; 111 + } 105 112 106 - .form-group input:focus { 107 - outline: none; 108 - border-color: var(--primary-500); 109 - box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.1); 110 - } 113 + .form-group input:focus { 114 + outline: none; 115 + border-color: var(--primary-500); 116 + box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.1); 117 + } 111 118 112 - .btn { 113 - padding: 0.75rem 1.5rem; 114 - border: none; 115 - border-radius: 0.375rem; 116 - font-size: 1rem; 117 - font-weight: 500; 118 - cursor: pointer; 119 - transition: background-color 0.15s; 120 - } 119 + .btn { 120 + padding: 0.75rem 1.5rem; 121 + border: none; 122 + border-radius: 0.375rem; 123 + font-size: 1rem; 124 + font-weight: 500; 125 + cursor: pointer; 126 + transition: background-color 0.15s; 127 + } 121 128 122 - .btn-primary { 123 - background: var(--primary-500); 124 - color: white; 125 - } 129 + .btn-primary { 130 + background: var(--primary-500); 131 + color: white; 132 + } 126 133 127 - .btn-primary:hover { 128 - background: var(--primary-600); 129 - } 134 + .btn-primary:hover { 135 + background: var(--primary-600); 136 + } 130 137 131 - .btn-primary:disabled { 132 - background: var(--gray-200); 133 - color: var(--gray-500); 134 - cursor: not-allowed; 135 - } 138 + .btn-primary:disabled { 139 + background: var(--gray-200); 140 + color: var(--gray-500); 141 + cursor: not-allowed; 142 + } 136 143 137 - .btn-secondary { 138 - background: var(--gray-200); 139 - color: var(--gray-700); 140 - } 144 + .btn-secondary { 145 + background: var(--gray-200); 146 + color: var(--gray-700); 147 + } 141 148 142 - .btn-secondary:hover { 143 - background: var(--border-color); 144 - } 149 + .btn-secondary:hover { 150 + background: var(--border-color); 151 + } 145 152 146 - /* User Card */ 147 - .user-card { 148 - display: flex; 149 - align-items: center; 150 - justify-content: space-between; 151 - } 153 + /* User Card */ 154 + .user-card { 155 + display: flex; 156 + align-items: center; 157 + justify-content: space-between; 158 + } 152 159 153 - .user-info { 154 - display: flex; 155 - align-items: center; 156 - gap: 0.75rem; 157 - } 160 + .user-info { 161 + display: flex; 162 + align-items: center; 163 + gap: 0.75rem; 164 + } 158 165 159 - .user-avatar { 160 - width: 48px; 161 - height: 48px; 162 - border-radius: 50%; 163 - background: var(--gray-200); 164 - display: flex; 165 - align-items: center; 166 - justify-content: center; 167 - font-size: 1.5rem; 168 - } 166 + .user-avatar { 167 + width: 48px; 168 + height: 48px; 169 + border-radius: 50%; 170 + background: var(--gray-200); 171 + display: flex; 172 + align-items: center; 173 + justify-content: center; 174 + font-size: 1.5rem; 175 + } 169 176 170 - .user-avatar img { 171 - width: 100%; 172 - height: 100%; 173 - border-radius: 50%; 174 - object-fit: cover; 175 - } 177 + .user-avatar img { 178 + width: 100%; 179 + height: 100%; 180 + border-radius: 50%; 181 + object-fit: cover; 182 + } 176 183 177 - .user-name { 178 - font-weight: 600; 179 - } 184 + .user-name { 185 + font-weight: 600; 186 + } 180 187 181 - .user-handle { 182 - font-size: 0.875rem; 183 - color: var(--gray-500); 184 - } 188 + .user-handle { 189 + font-size: 0.875rem; 190 + color: var(--gray-500); 191 + } 185 192 186 - /* Emoji Picker */ 187 - .emoji-grid { 188 - display: grid; 189 - grid-template-columns: repeat(9, 1fr); 190 - gap: 0.5rem; 191 - } 193 + /* Emoji Picker */ 194 + .emoji-grid { 195 + display: grid; 196 + grid-template-columns: repeat(9, 1fr); 197 + gap: 0.5rem; 198 + } 192 199 193 - .emoji-btn { 194 - width: 100%; 195 - aspect-ratio: 1; 196 - font-size: 1.5rem; 197 - border: 2px solid var(--border-color); 198 - border-radius: 50%; 199 - background: white; 200 - cursor: pointer; 201 - transition: all 0.15s; 202 - display: flex; 203 - align-items: center; 204 - justify-content: center; 205 - } 200 + .emoji-btn { 201 + width: 100%; 202 + aspect-ratio: 1; 203 + font-size: 1.5rem; 204 + border: 2px solid var(--border-color); 205 + border-radius: 50%; 206 + background: white; 207 + cursor: pointer; 208 + transition: all 0.15s; 209 + display: flex; 210 + align-items: center; 211 + justify-content: center; 212 + } 206 213 207 - .emoji-btn:hover { 208 - background: rgba(0, 120, 255, 0.1); 209 - border-color: var(--primary-400); 210 - } 214 + .emoji-btn:hover { 215 + background: rgba(0, 120, 255, 0.1); 216 + border-color: var(--primary-400); 217 + } 211 218 212 - .emoji-btn.selected { 213 - border-color: var(--primary-500); 214 - box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.2); 215 - } 219 + .emoji-btn.selected { 220 + border-color: var(--primary-500); 221 + box-shadow: 0 0 0 3px rgba(0, 120, 255, 0.2); 222 + } 216 223 217 - .emoji-btn:disabled { 218 - opacity: 0.5; 219 - cursor: not-allowed; 220 - } 224 + .emoji-btn:disabled { 225 + opacity: 0.5; 226 + cursor: not-allowed; 227 + } 221 228 222 - .emoji-btn:disabled:hover { 223 - background: white; 224 - border-color: var(--border-color); 225 - } 229 + .emoji-btn:disabled:hover { 230 + background: white; 231 + border-color: var(--border-color); 232 + } 226 233 227 - /* Status Feed */ 228 - .feed-title { 229 - font-size: 1.125rem; 230 - font-weight: 600; 231 - margin-bottom: 1rem; 232 - color: var(--gray-700); 233 - } 234 + /* Status Feed */ 235 + .feed-title { 236 + font-size: 1.125rem; 237 + font-weight: 600; 238 + margin-bottom: 1rem; 239 + color: var(--gray-700); 240 + } 234 241 235 - .status-list { 236 - list-style: none; 237 - padding: 0; 238 - } 242 + .status-list { 243 + list-style: none; 244 + padding: 0; 245 + } 239 246 240 - .status-item { 241 - position: relative; 242 - padding-left: 2rem; 243 - padding-bottom: 1.5rem; 244 - } 247 + .status-item { 248 + position: relative; 249 + padding-left: 2rem; 250 + padding-bottom: 1.5rem; 251 + } 245 252 246 - .status-item::before { 247 - content: ""; 248 - position: absolute; 249 - left: 0.75rem; 250 - top: 1.5rem; 251 - bottom: 0; 252 - width: 2px; 253 - background: var(--border-color); 254 - } 253 + .status-item::before { 254 + content: ""; 255 + position: absolute; 256 + left: 0.75rem; 257 + top: 1.5rem; 258 + bottom: 0; 259 + width: 2px; 260 + background: var(--border-color); 261 + } 255 262 256 - .status-item:last-child::before { 257 - display: none; 258 - } 263 + .status-item:last-child::before { 264 + display: none; 265 + } 259 266 260 - .status-item:last-child { 261 - padding-bottom: 0; 262 - } 267 + .status-item:last-child { 268 + padding-bottom: 0; 269 + } 263 270 264 - .status-emoji { 265 - position: absolute; 266 - left: 0; 267 - top: 0; 268 - font-size: 1.5rem; 269 - } 271 + .status-emoji { 272 + position: absolute; 273 + left: 0; 274 + top: 0; 275 + font-size: 1.5rem; 276 + } 270 277 271 - .status-content { 272 - padding-top: 0.25rem; 273 - } 278 + .status-content { 279 + padding-top: 0.25rem; 280 + } 274 281 275 - .status-author { 276 - color: var(--primary-500); 277 - text-decoration: none; 278 - font-weight: 500; 279 - } 282 + .status-author { 283 + color: var(--primary-500); 284 + text-decoration: none; 285 + font-weight: 500; 286 + } 280 287 281 - .status-author:hover { 282 - text-decoration: underline; 283 - } 288 + .status-author:hover { 289 + text-decoration: underline; 290 + } 284 291 285 - .status-text { 286 - color: var(--gray-700); 287 - } 292 + .status-text { 293 + color: var(--gray-700); 294 + } 288 295 289 - .status-date { 290 - font-size: 0.875rem; 291 - color: var(--gray-500); 292 - } 296 + .status-date { 297 + font-size: 0.875rem; 298 + color: var(--gray-500); 299 + } 293 300 294 - /* Error Banner */ 295 - #error-banner { 296 - position: fixed; 297 - top: 1rem; 298 - left: 50%; 299 - transform: translateX(-50%); 300 - background: var(--error-bg); 301 - border: 1px solid var(--error-border); 302 - color: var(--error-text); 303 - padding: 0.75rem 1rem; 304 - border-radius: 0.375rem; 305 - display: flex; 306 - align-items: center; 307 - gap: 0.75rem; 308 - max-width: 90%; 309 - z-index: 100; 310 - } 301 + /* Error Banner */ 302 + #error-banner { 303 + position: fixed; 304 + top: 1rem; 305 + left: 50%; 306 + transform: translateX(-50%); 307 + background: var(--error-bg); 308 + border: 1px solid var(--error-border); 309 + color: var(--error-text); 310 + padding: 0.75rem 1rem; 311 + border-radius: 0.375rem; 312 + display: flex; 313 + align-items: center; 314 + gap: 0.75rem; 315 + max-width: 90%; 316 + z-index: 100; 317 + } 311 318 312 - #error-banner.hidden { 313 - display: none; 314 - } 319 + #error-banner.hidden { 320 + display: none; 321 + } 315 322 316 - #error-banner button { 317 - background: none; 318 - border: none; 319 - color: var(--error-text); 320 - cursor: pointer; 321 - font-size: 1.25rem; 322 - line-height: 1; 323 - } 323 + #error-banner button { 324 + background: none; 325 + border: none; 326 + color: var(--error-text); 327 + cursor: pointer; 328 + font-size: 1.25rem; 329 + line-height: 1; 330 + } 324 331 325 - /* Loading State */ 326 - .loading { 327 - text-align: center; 328 - color: var(--gray-500); 329 - padding: 2rem; 330 - } 332 + /* Loading State */ 333 + .loading { 334 + text-align: center; 335 + color: var(--gray-500); 336 + padding: 2rem; 337 + } 331 338 332 - /* Responsive */ 333 - @media (max-width: 480px) { 334 - .emoji-grid { 335 - grid-template-columns: repeat(6, 1fr); 336 - } 339 + /* Responsive */ 340 + @media (max-width: 480px) { 341 + .emoji-grid { 342 + grid-template-columns: repeat(6, 1fr); 343 + } 337 344 338 - .emoji-btn { 339 - font-size: 1.25rem; 340 - } 341 - } 345 + .emoji-btn { 346 + font-size: 1.25rem; 347 + } 348 + } 342 349 343 - /* Hidden utility */ 344 - .hidden { 345 - display: none !important; 346 - } 347 - </style> 348 - </head> 349 - <body> 350 - <div id="app"> 351 - <header> 352 - <h1>Statusphere</h1> 353 - <p class="tagline">Set your status on the Atmosphere</p> 354 - </header> 355 - <main> 356 - <div id="auth-section"></div> 357 - <div id="emoji-picker"></div> 358 - <div id="status-feed"></div> 359 - </main> 360 - <div id="error-banner" class="hidden"></div> 361 - </div> 362 - <script> 363 - // ============================================================================= 364 - // CONSTANTS 365 - // ============================================================================= 350 + /* Hidden utility */ 351 + .hidden { 352 + display: none !important; 353 + } 354 + </style> 355 + </head> 356 + <body> 357 + <div id="app"> 358 + <header> 359 + <h1>Statusphere</h1> 360 + <p class="tagline">Set your status on the Atmosphere</p> 361 + </header> 362 + <main> 363 + <div id="auth-section"></div> 364 + <div id="emoji-picker"></div> 365 + <div id="status-feed"></div> 366 + </main> 367 + <div id="error-banner" class="hidden"></div> 368 + </div> 369 + <script> 370 + // ============================================================================= 371 + // CONSTANTS 372 + // ============================================================================= 366 373 367 - const GRAPHQL_URL = 'http://localhost:8080/graphql'; 368 - const OAUTH_AUTHORIZE_URL = 'http://localhost:8080/oauth/authorize'; 369 - const OAUTH_TOKEN_URL = 'http://localhost:8080/oauth/token'; 374 + const GRAPHQL_URL = "http://localhost:8080/graphql"; 375 + const OAUTH_AUTHORIZE_URL = "http://localhost:8080/oauth/authorize"; 376 + const OAUTH_TOKEN_URL = "http://localhost:8080/oauth/token"; 370 377 371 - const EMOJIS = [ 372 - '👍', '👎', '💙', '😧', '😤', '🙃', '😉', '😎', '🤩', 373 - '🥳', '😭', '😱', '🥺', '😡', '💀', '🤖', '👻', '👽', 374 - '🎃', '🤡', '💩', '🔥', '⭐', '🌈', '🍕', '🎉', '💯' 375 - ]; 378 + const EMOJIS = [ 379 + "👍", 380 + "👎", 381 + "💙", 382 + "😧", 383 + "😤", 384 + "🙃", 385 + "😉", 386 + "😎", 387 + "🤩", 388 + "🥳", 389 + "😭", 390 + "😱", 391 + "🥺", 392 + "😡", 393 + "💀", 394 + "🤖", 395 + "👻", 396 + "👽", 397 + "🎃", 398 + "🤡", 399 + "💩", 400 + "🔥", 401 + "⭐", 402 + "🌈", 403 + "🍕", 404 + "🎉", 405 + "💯", 406 + ]; 376 407 377 - const STORAGE_KEYS = { 378 - accessToken: 'qs_access_token', 379 - refreshToken: 'qs_refresh_token', 380 - userDid: 'qs_user_did', 381 - codeVerifier: 'qs_code_verifier', 382 - oauthState: 'qs_oauth_state', 383 - clientId: 'qs_client_id' 384 - }; 408 + const STORAGE_KEYS = { 409 + accessToken: "qs_access_token", 410 + refreshToken: "qs_refresh_token", 411 + userDid: "qs_user_did", 412 + codeVerifier: "qs_code_verifier", 413 + oauthState: "qs_oauth_state", 414 + clientId: "qs_client_id", 415 + }; 385 416 386 - // ============================================================================= 387 - // STORAGE UTILITIES 388 - // ============================================================================= 417 + // ============================================================================= 418 + // STORAGE UTILITIES 419 + // ============================================================================= 389 420 390 - const storage = { 391 - get(key) { 392 - return sessionStorage.getItem(key); 393 - }, 394 - set(key, value) { 395 - sessionStorage.setItem(key, value); 396 - }, 397 - remove(key) { 398 - sessionStorage.removeItem(key); 399 - }, 400 - clear() { 401 - Object.values(STORAGE_KEYS).forEach(key => sessionStorage.removeItem(key)); 402 - } 403 - }; 421 + const storage = { 422 + get(key) { 423 + return sessionStorage.getItem(key); 424 + }, 425 + set(key, value) { 426 + sessionStorage.setItem(key, value); 427 + }, 428 + remove(key) { 429 + sessionStorage.removeItem(key); 430 + }, 431 + clear() { 432 + Object.values(STORAGE_KEYS).forEach((key) => 433 + sessionStorage.removeItem(key), 434 + ); 435 + }, 436 + }; 404 437 405 - // ============================================================================= 406 - // PKCE UTILITIES 407 - // ============================================================================= 438 + // ============================================================================= 439 + // PKCE UTILITIES 440 + // ============================================================================= 408 441 409 - function base64UrlEncode(buffer) { 410 - const bytes = new Uint8Array(buffer); 411 - let binary = ''; 412 - for (let i = 0; i < bytes.length; i++) { 413 - binary += String.fromCharCode(bytes[i]); 414 - } 415 - return btoa(binary) 416 - .replace(/\+/g, '-') 417 - .replace(/\//g, '_') 418 - .replace(/=+$/, ''); 419 - } 442 + function base64UrlEncode(buffer) { 443 + const bytes = new Uint8Array(buffer); 444 + let binary = ""; 445 + for (let i = 0; i < bytes.length; i++) { 446 + binary += String.fromCharCode(bytes[i]); 447 + } 448 + return btoa(binary) 449 + .replace(/\+/g, "-") 450 + .replace(/\//g, "_") 451 + .replace(/=+$/, ""); 452 + } 420 453 421 - async function generateCodeVerifier() { 422 - const randomBytes = new Uint8Array(32); 423 - crypto.getRandomValues(randomBytes); 424 - return base64UrlEncode(randomBytes); 425 - } 454 + async function generateCodeVerifier() { 455 + const randomBytes = new Uint8Array(32); 456 + crypto.getRandomValues(randomBytes); 457 + return base64UrlEncode(randomBytes); 458 + } 426 459 427 - async function generateCodeChallenge(verifier) { 428 - const encoder = new TextEncoder(); 429 - const data = encoder.encode(verifier); 430 - const hash = await crypto.subtle.digest('SHA-256', data); 431 - return base64UrlEncode(hash); 432 - } 460 + async function generateCodeChallenge(verifier) { 461 + const encoder = new TextEncoder(); 462 + const data = encoder.encode(verifier); 463 + const hash = await crypto.subtle.digest("SHA-256", data); 464 + return base64UrlEncode(hash); 465 + } 433 466 434 - function generateState() { 435 - const randomBytes = new Uint8Array(16); 436 - crypto.getRandomValues(randomBytes); 437 - return base64UrlEncode(randomBytes); 438 - } 467 + function generateState() { 468 + const randomBytes = new Uint8Array(16); 469 + crypto.getRandomValues(randomBytes); 470 + return base64UrlEncode(randomBytes); 471 + } 439 472 440 - // ============================================================================= 441 - // OAUTH FUNCTIONS 442 - // ============================================================================= 473 + // ============================================================================= 474 + // OAUTH FUNCTIONS 475 + // ============================================================================= 443 476 444 - async function initiateLogin(clientId, handle) { 445 - const codeVerifier = await generateCodeVerifier(); 446 - const codeChallenge = await generateCodeChallenge(codeVerifier); 447 - const state = generateState(); 477 + async function initiateLogin(clientId, handle) { 478 + const codeVerifier = await generateCodeVerifier(); 479 + const codeChallenge = await generateCodeChallenge(codeVerifier); 480 + const state = generateState(); 448 481 449 - // Store for callback 450 - storage.set(STORAGE_KEYS.codeVerifier, codeVerifier); 451 - storage.set(STORAGE_KEYS.oauthState, state); 452 - storage.set(STORAGE_KEYS.clientId, clientId); 482 + // Store for callback 483 + storage.set(STORAGE_KEYS.codeVerifier, codeVerifier); 484 + storage.set(STORAGE_KEYS.oauthState, state); 485 + storage.set(STORAGE_KEYS.clientId, clientId); 453 486 454 - // Build redirect URI (current page without query params) 455 - const redirectUri = window.location.origin + window.location.pathname; 487 + // Build redirect URI (current page without query params) 488 + const redirectUri = window.location.origin + window.location.pathname; 456 489 457 - // Build authorization URL 458 - const params = new URLSearchParams({ 459 - client_id: clientId, 460 - redirect_uri: redirectUri, 461 - response_type: 'code', 462 - code_challenge: codeChallenge, 463 - code_challenge_method: 'S256', 464 - state: state, 465 - login_hint: handle 466 - }); 490 + // Build authorization URL 491 + const params = new URLSearchParams({ 492 + client_id: clientId, 493 + redirect_uri: redirectUri, 494 + response_type: "code", 495 + code_challenge: codeChallenge, 496 + code_challenge_method: "S256", 497 + state: state, 498 + login_hint: handle, 499 + }); 467 500 468 - window.location.href = `${OAUTH_AUTHORIZE_URL}?${params.toString()}`; 469 - } 501 + window.location.href = `${OAUTH_AUTHORIZE_URL}?${params.toString()}`; 502 + } 470 503 471 - async function handleOAuthCallback() { 472 - const params = new URLSearchParams(window.location.search); 473 - const code = params.get('code'); 474 - const state = params.get('state'); 475 - const error = params.get('error'); 504 + async function handleOAuthCallback() { 505 + const params = new URLSearchParams(window.location.search); 506 + const code = params.get("code"); 507 + const state = params.get("state"); 508 + const error = params.get("error"); 476 509 477 - if (error) { 478 - throw new Error(`OAuth error: ${error} - ${params.get('error_description') || ''}`); 479 - } 510 + if (error) { 511 + throw new Error( 512 + `OAuth error: ${error} - ${params.get("error_description") || ""}`, 513 + ); 514 + } 480 515 481 - if (!code || !state) { 482 - return false; // Not a callback 483 - } 516 + if (!code || !state) { 517 + return false; // Not a callback 518 + } 484 519 485 - // Verify state 486 - const storedState = storage.get(STORAGE_KEYS.oauthState); 487 - if (state !== storedState) { 488 - throw new Error('OAuth state mismatch - possible CSRF attack'); 489 - } 520 + // Verify state 521 + const storedState = storage.get(STORAGE_KEYS.oauthState); 522 + if (state !== storedState) { 523 + throw new Error("OAuth state mismatch - possible CSRF attack"); 524 + } 490 525 491 - // Get stored values 492 - const codeVerifier = storage.get(STORAGE_KEYS.codeVerifier); 493 - const clientId = storage.get(STORAGE_KEYS.clientId); 494 - const redirectUri = window.location.origin + window.location.pathname; 526 + // Get stored values 527 + const codeVerifier = storage.get(STORAGE_KEYS.codeVerifier); 528 + const clientId = storage.get(STORAGE_KEYS.clientId); 529 + const redirectUri = window.location.origin + window.location.pathname; 495 530 496 - if (!codeVerifier || !clientId) { 497 - throw new Error('Missing OAuth session data'); 498 - } 531 + if (!codeVerifier || !clientId) { 532 + throw new Error("Missing OAuth session data"); 533 + } 499 534 500 - // Exchange code for tokens 501 - const tokenResponse = await fetch(OAUTH_TOKEN_URL, { 502 - method: 'POST', 503 - headers: { 504 - 'Content-Type': 'application/x-www-form-urlencoded' 505 - }, 506 - body: new URLSearchParams({ 507 - grant_type: 'authorization_code', 508 - code: code, 509 - redirect_uri: redirectUri, 510 - client_id: clientId, 511 - code_verifier: codeVerifier 512 - }) 513 - }); 535 + // Exchange code for tokens 536 + const tokenResponse = await fetch(OAUTH_TOKEN_URL, { 537 + method: "POST", 538 + headers: { 539 + "Content-Type": "application/x-www-form-urlencoded", 540 + }, 541 + body: new URLSearchParams({ 542 + grant_type: "authorization_code", 543 + code: code, 544 + redirect_uri: redirectUri, 545 + client_id: clientId, 546 + code_verifier: codeVerifier, 547 + }), 548 + }); 514 549 515 - if (!tokenResponse.ok) { 516 - const errorData = await tokenResponse.json().catch(() => ({})); 517 - throw new Error(`Token exchange failed: ${errorData.error_description || tokenResponse.statusText}`); 518 - } 550 + if (!tokenResponse.ok) { 551 + const errorData = await tokenResponse.json().catch(() => ({})); 552 + throw new Error( 553 + `Token exchange failed: ${errorData.error_description || tokenResponse.statusText}`, 554 + ); 555 + } 519 556 520 - const tokens = await tokenResponse.json(); 557 + const tokens = await tokenResponse.json(); 521 558 522 - // Store tokens 523 - storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 524 - if (tokens.refresh_token) { 525 - storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 526 - } 559 + // Store tokens 560 + storage.set(STORAGE_KEYS.accessToken, tokens.access_token); 561 + if (tokens.refresh_token) { 562 + storage.set(STORAGE_KEYS.refreshToken, tokens.refresh_token); 563 + } 527 564 528 - // Extract DID from token response (sub claim) or we'll fetch it later 529 - if (tokens.sub) { 530 - storage.set(STORAGE_KEYS.userDid, tokens.sub); 531 - } 565 + // Extract DID from token response (sub claim) or we'll fetch it later 566 + if (tokens.sub) { 567 + storage.set(STORAGE_KEYS.userDid, tokens.sub); 568 + } 532 569 533 - // Clean up OAuth state 534 - storage.remove(STORAGE_KEYS.codeVerifier); 535 - storage.remove(STORAGE_KEYS.oauthState); 570 + // Clean up OAuth state 571 + storage.remove(STORAGE_KEYS.codeVerifier); 572 + storage.remove(STORAGE_KEYS.oauthState); 536 573 537 - // Clear URL params 538 - window.history.replaceState({}, document.title, window.location.pathname); 574 + // Clear URL params 575 + window.history.replaceState( 576 + {}, 577 + document.title, 578 + window.location.pathname, 579 + ); 539 580 540 - return true; 541 - } 581 + return true; 582 + } 542 583 543 - function logout() { 544 - storage.clear(); 545 - window.location.reload(); 546 - } 584 + function logout() { 585 + storage.clear(); 586 + window.location.reload(); 587 + } 547 588 548 - function isLoggedIn() { 549 - return !!storage.get(STORAGE_KEYS.accessToken); 550 - } 589 + function isLoggedIn() { 590 + return !!storage.get(STORAGE_KEYS.accessToken); 591 + } 551 592 552 - function getAccessToken() { 553 - return storage.get(STORAGE_KEYS.accessToken); 554 - } 593 + function getAccessToken() { 594 + return storage.get(STORAGE_KEYS.accessToken); 595 + } 555 596 556 - function getUserDid() { 557 - return storage.get(STORAGE_KEYS.userDid); 558 - } 597 + function getUserDid() { 598 + return storage.get(STORAGE_KEYS.userDid); 599 + } 559 600 560 - // ============================================================================= 561 - // GRAPHQL UTILITIES 562 - // ============================================================================= 601 + // ============================================================================= 602 + // GRAPHQL UTILITIES 603 + // ============================================================================= 563 604 564 - async function graphqlQuery(query, variables = {}, requireAuth = false) { 565 - const headers = { 566 - 'Content-Type': 'application/json' 567 - }; 605 + async function graphqlQuery(query, variables = {}, requireAuth = false) { 606 + const headers = { 607 + "Content-Type": "application/json", 608 + }; 568 609 569 - if (requireAuth) { 570 - const token = getAccessToken(); 571 - if (!token) { 572 - throw new Error('Not authenticated'); 573 - } 574 - headers['Authorization'] = `Bearer ${token}`; 575 - } 610 + if (requireAuth) { 611 + const token = getAccessToken(); 612 + if (!token) { 613 + throw new Error("Not authenticated"); 614 + } 615 + headers["Authorization"] = `Bearer ${token}`; 616 + } 576 617 577 - const response = await fetch(GRAPHQL_URL, { 578 - method: 'POST', 579 - headers, 580 - body: JSON.stringify({ query, variables }) 581 - }); 618 + const response = await fetch(GRAPHQL_URL, { 619 + method: "POST", 620 + headers, 621 + body: JSON.stringify({ query, variables }), 622 + }); 582 623 583 - if (!response.ok) { 584 - throw new Error(`GraphQL request failed: ${response.statusText}`); 585 - } 624 + if (!response.ok) { 625 + throw new Error(`GraphQL request failed: ${response.statusText}`); 626 + } 586 627 587 - const result = await response.json(); 628 + const result = await response.json(); 588 629 589 - if (result.errors && result.errors.length > 0) { 590 - throw new Error(`GraphQL error: ${result.errors[0].message}`); 591 - } 630 + if (result.errors && result.errors.length > 0) { 631 + throw new Error(`GraphQL error: ${result.errors[0].message}`); 632 + } 592 633 593 - return result.data; 594 - } 634 + return result.data; 635 + } 595 636 596 - // ============================================================================= 597 - // DATA FETCHING 598 - // ============================================================================= 637 + // ============================================================================= 638 + // DATA FETCHING 639 + // ============================================================================= 599 640 600 - async function fetchStatuses() { 601 - const query = ` 641 + async function fetchStatuses() { 642 + const query = ` 602 643 query GetStatuses { 603 644 xyzStatusphereStatus( 604 645 first: 20 ··· 620 661 } 621 662 `; 622 663 623 - const data = await graphqlQuery(query); 624 - return data.xyzStatusphereStatus?.edges?.map(e => e.node) || []; 625 - } 664 + const data = await graphqlQuery(query); 665 + return data.xyzStatusphereStatus?.edges?.map((e) => e.node) || []; 666 + } 626 667 627 - async function fetchViewer() { 628 - const query = ` 668 + async function fetchViewer() { 669 + const query = ` 629 670 query { 630 671 viewer { 631 672 did ··· 638 679 } 639 680 `; 640 681 641 - const data = await graphqlQuery(query, {}, true); 642 - return data?.viewer; 643 - } 682 + const data = await graphqlQuery(query, {}, true); 683 + return data?.viewer; 684 + } 644 685 645 - async function postStatus(emoji) { 646 - const mutation = ` 686 + async function postStatus(emoji) { 687 + const mutation = ` 647 688 mutation CreateStatus($status: String!, $createdAt: DateTime!) { 648 689 createXyzStatusphereStatus( 649 690 input: { status: $status, createdAt: $createdAt } ··· 655 696 } 656 697 `; 657 698 658 - const variables = { 659 - status: emoji, 660 - createdAt: new Date().toISOString() 661 - }; 699 + const variables = { 700 + status: emoji, 701 + createdAt: new Date().toISOString(), 702 + }; 662 703 663 - const data = await graphqlQuery(mutation, variables, true); 664 - return data.createXyzStatusphereStatus; 665 - } 704 + const data = await graphqlQuery(mutation, variables, true); 705 + return data.createXyzStatusphereStatus; 706 + } 666 707 667 - // ============================================================================= 668 - // UI RENDERING 669 - // ============================================================================= 708 + // ============================================================================= 709 + // UI RENDERING 710 + // ============================================================================= 670 711 671 - function showError(message) { 672 - const banner = document.getElementById('error-banner'); 673 - banner.innerHTML = ` 712 + function showError(message) { 713 + const banner = document.getElementById("error-banner"); 714 + banner.innerHTML = ` 674 715 <span>${escapeHtml(message)}</span> 675 716 <button onclick="hideError()">&times;</button> 676 717 `; 677 - banner.classList.remove('hidden'); 678 - } 718 + banner.classList.remove("hidden"); 719 + } 679 720 680 - function hideError() { 681 - document.getElementById('error-banner').classList.add('hidden'); 682 - } 721 + function hideError() { 722 + document.getElementById("error-banner").classList.add("hidden"); 723 + } 683 724 684 - function escapeHtml(text) { 685 - const div = document.createElement('div'); 686 - div.textContent = text; 687 - return div.innerHTML; 688 - } 725 + function escapeHtml(text) { 726 + const div = document.createElement("div"); 727 + div.textContent = text; 728 + return div.innerHTML; 729 + } 689 730 690 - function formatDate(dateString) { 691 - const date = new Date(dateString); 692 - const now = new Date(); 693 - const isToday = date.toDateString() === now.toDateString(); 731 + function formatDate(dateString) { 732 + const date = new Date(dateString); 733 + const now = new Date(); 734 + const isToday = date.toDateString() === now.toDateString(); 694 735 695 - if (isToday) { 696 - return 'today'; 697 - } 736 + if (isToday) { 737 + return "today"; 738 + } 698 739 699 - return date.toLocaleDateString('en-US', { 700 - month: 'short', 701 - day: 'numeric', 702 - year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined 703 - }); 704 - } 740 + return date.toLocaleDateString("en-US", { 741 + month: "short", 742 + day: "numeric", 743 + year: 744 + date.getFullYear() !== now.getFullYear() ? "numeric" : undefined, 745 + }); 746 + } 705 747 706 - function renderLoginForm() { 707 - const container = document.getElementById('auth-section'); 708 - const savedClientId = storage.get(STORAGE_KEYS.clientId) || ''; 748 + function renderLoginForm() { 749 + const container = document.getElementById("auth-section"); 750 + const savedClientId = storage.get(STORAGE_KEYS.clientId) || ""; 709 751 710 - container.innerHTML = ` 752 + container.innerHTML = ` 711 753 <div class="card"> 712 754 <form class="login-form" onsubmit="handleLogin(event)"> 713 755 <div class="form-group"> ··· 736 778 </p> 737 779 </div> 738 780 `; 739 - } 781 + } 740 782 741 - function renderUserCard(profile) { 742 - const container = document.getElementById('auth-section'); 743 - const displayName = profile?.displayName || 'User'; 744 - const handle = profile?.actorHandle || 'unknown'; 745 - const avatarUrl = profile?.avatar?.url; 783 + function renderUserCard(profile) { 784 + const container = document.getElementById("auth-section"); 785 + const displayName = profile?.displayName || "User"; 786 + const handle = profile?.actorHandle || "unknown"; 787 + const avatarUrl = profile?.avatar?.url; 746 788 747 - container.innerHTML = ` 789 + container.innerHTML = ` 748 790 <div class="card user-card"> 749 791 <div class="user-info"> 750 792 <div class="user-avatar"> 751 - ${avatarUrl 752 - ? `<img src="${escapeHtml(avatarUrl)}" alt="Avatar">` 753 - : '👤'} 793 + ${ 794 + avatarUrl 795 + ? `<img src="${escapeHtml(avatarUrl)}" alt="Avatar">` 796 + : "👤" 797 + } 754 798 </div> 755 799 <div> 756 800 <div class="user-name">Hi, ${escapeHtml(displayName)}!</div> ··· 760 804 <button class="btn btn-secondary" onclick="logout()">Logout</button> 761 805 </div> 762 806 `; 763 - } 807 + } 764 808 765 - function renderEmojiPicker(currentStatus, enabled = true) { 766 - const container = document.getElementById('emoji-picker'); 809 + function renderEmojiPicker(currentStatus, enabled = true) { 810 + const container = document.getElementById("emoji-picker"); 767 811 768 - container.innerHTML = ` 812 + container.innerHTML = ` 769 813 <div class="card"> 770 814 <div class="emoji-grid"> 771 - ${EMOJIS.map(emoji => ` 815 + ${EMOJIS.map( 816 + (emoji) => ` 772 817 <button 773 - class="emoji-btn ${emoji === currentStatus ? 'selected' : ''}" 818 + class="emoji-btn ${emoji === currentStatus ? "selected" : ""}" 774 819 onclick="selectStatus('${emoji}')" 775 - ${!enabled ? 'disabled' : ''} 776 - title="${enabled ? 'Set status' : 'Login to set status'}" 820 + ${!enabled ? "disabled" : ""} 821 + title="${enabled ? "Set status" : "Login to set status"}" 777 822 > 778 823 ${emoji} 779 824 </button> 780 - `).join('')} 825 + `, 826 + ).join("")} 781 827 </div> 782 828 </div> 783 829 `; 784 - } 830 + } 785 831 786 - function renderStatusFeed(statuses) { 787 - const container = document.getElementById('status-feed'); 832 + function renderStatusFeed(statuses) { 833 + const container = document.getElementById("status-feed"); 788 834 789 - if (statuses.length === 0) { 790 - container.innerHTML = ` 835 + if (statuses.length === 0) { 836 + container.innerHTML = ` 791 837 <div class="card"> 792 838 <p class="loading">No statuses yet. Be the first to post!</p> 793 839 </div> 794 840 `; 795 - return; 796 - } 841 + return; 842 + } 797 843 798 - container.innerHTML = ` 844 + container.innerHTML = ` 799 845 <div class="card"> 800 846 <h2 class="feed-title">Recent Statuses</h2> 801 847 <ul class="status-list"> 802 - ${statuses.map(status => { 803 - const handle = status.appBskyActorProfileByDid?.actorHandle || status.did; 804 - const displayHandle = handle.startsWith('did:') ? handle.substring(0, 20) + '...' : handle; 805 - const profileUrl = handle.startsWith('did:') 806 - ? `https://bsky.app/profile/${status.did}` 807 - : `https://bsky.app/profile/${handle}`; 848 + ${statuses 849 + .map((status) => { 850 + const handle = 851 + status.appBskyActorProfileByDid?.actorHandle || status.did; 852 + const displayHandle = handle.startsWith("did:") 853 + ? handle.substring(0, 20) + "..." 854 + : handle; 855 + const profileUrl = handle.startsWith("did:") 856 + ? `https://bsky.app/profile/${status.did}` 857 + : `https://bsky.app/profile/${handle}`; 808 858 809 - return ` 859 + return ` 810 860 <li class="status-item"> 811 861 <span class="status-emoji">${status.status}</span> 812 862 <div class="status-content"> ··· 818 868 </div> 819 869 </li> 820 870 `; 821 - }).join('')} 871 + }) 872 + .join("")} 822 873 </ul> 823 874 </div> 824 875 `; 825 - } 876 + } 826 877 827 - function renderLoading(container) { 828 - document.getElementById(container).innerHTML = ` 878 + function renderLoading(container) { 879 + document.getElementById(container).innerHTML = ` 829 880 <div class="card"> 830 881 <p class="loading">Loading...</p> 831 882 </div> 832 883 `; 833 - } 884 + } 834 885 835 - // ============================================================================= 836 - // EVENT HANDLERS 837 - // ============================================================================= 886 + // ============================================================================= 887 + // EVENT HANDLERS 888 + // ============================================================================= 838 889 839 - async function handleLogin(event) { 840 - event.preventDefault(); 890 + async function handleLogin(event) { 891 + event.preventDefault(); 841 892 842 - const clientId = document.getElementById('client-id').value.trim(); 843 - const handle = document.getElementById('handle').value.trim(); 893 + const clientId = document.getElementById("client-id").value.trim(); 894 + const handle = document.getElementById("handle").value.trim(); 844 895 845 - if (!clientId || !handle) { 846 - showError('Please enter both Client ID and Handle'); 847 - return; 848 - } 896 + if (!clientId || !handle) { 897 + showError("Please enter both Client ID and Handle"); 898 + return; 899 + } 849 900 850 - try { 851 - await initiateLogin(clientId, handle); 852 - } catch (error) { 853 - showError(`Login failed: ${error.message}`); 854 - } 855 - } 901 + try { 902 + await initiateLogin(clientId, handle); 903 + } catch (error) { 904 + showError(`Login failed: ${error.message}`); 905 + } 906 + } 856 907 857 - async function selectStatus(emoji) { 858 - if (!isLoggedIn()) { 859 - showError('Please login to set your status'); 860 - return; 861 - } 908 + async function selectStatus(emoji) { 909 + if (!isLoggedIn()) { 910 + showError("Please login to set your status"); 911 + return; 912 + } 862 913 863 - try { 864 - // Disable buttons while posting 865 - document.querySelectorAll('.emoji-btn').forEach(btn => btn.disabled = true); 914 + try { 915 + // Disable buttons while posting 916 + document 917 + .querySelectorAll(".emoji-btn") 918 + .forEach((btn) => (btn.disabled = true)); 866 919 867 - await postStatus(emoji); 920 + await postStatus(emoji); 868 921 869 - // Refresh the page to show new status 870 - window.location.reload(); 871 - } catch (error) { 872 - showError(`Failed to post status: ${error.message}`); 873 - // Re-enable buttons 874 - document.querySelectorAll('.emoji-btn').forEach(btn => btn.disabled = false); 875 - } 876 - } 922 + // Refresh the page to show new status 923 + window.location.reload(); 924 + } catch (error) { 925 + showError(`Failed to post status: ${error.message}`); 926 + // Re-enable buttons 927 + document 928 + .querySelectorAll(".emoji-btn") 929 + .forEach((btn) => (btn.disabled = false)); 930 + } 931 + } 877 932 878 - // ============================================================================= 879 - // MAIN INITIALIZATION 880 - // ============================================================================= 933 + // ============================================================================= 934 + // MAIN INITIALIZATION 935 + // ============================================================================= 881 936 882 - async function main() { 883 - try { 884 - // Check if this is an OAuth callback 885 - const isCallback = await handleOAuthCallback(); 886 - if (isCallback) { 887 - console.log('OAuth callback handled successfully'); 888 - } 889 - } catch (error) { 890 - showError(`Authentication failed: ${error.message}`); 891 - storage.clear(); 892 - } 937 + async function main() { 938 + try { 939 + // Check if this is an OAuth callback 940 + const isCallback = await handleOAuthCallback(); 941 + if (isCallback) { 942 + console.log("OAuth callback handled successfully"); 943 + } 944 + } catch (error) { 945 + showError(`Authentication failed: ${error.message}`); 946 + storage.clear(); 947 + } 893 948 894 - // Render auth section 895 - if (isLoggedIn()) { 896 - try { 897 - const viewer = await fetchViewer(); 898 - if (viewer) { 899 - const profile = { 900 - did: viewer.did, 901 - actorHandle: viewer.handle, 902 - displayName: viewer.appBskyActorProfileByDid?.displayName, 903 - avatar: viewer.appBskyActorProfileByDid?.avatar, 904 - }; 905 - renderUserCard(profile); 906 - } else { 907 - renderUserCard(null); 908 - } 909 - } catch (error) { 910 - console.error('Failed to fetch viewer:', error); 911 - renderUserCard(null); 912 - } 913 - } else { 914 - renderLoginForm(); 915 - } 949 + // Render auth section 950 + if (isLoggedIn()) { 951 + try { 952 + const viewer = await fetchViewer(); 953 + if (viewer) { 954 + const profile = { 955 + did: viewer.did, 956 + actorHandle: viewer.handle, 957 + displayName: viewer.appBskyActorProfileByDid?.displayName, 958 + avatar: viewer.appBskyActorProfileByDid?.avatar, 959 + }; 960 + renderUserCard(profile); 961 + } else { 962 + renderUserCard(null); 963 + } 964 + } catch (error) { 965 + console.error("Failed to fetch viewer:", error); 966 + renderUserCard(null); 967 + } 968 + } else { 969 + renderLoginForm(); 970 + } 916 971 917 - // Render emoji picker (enabled only if logged in) 918 - renderEmojiPicker(null, isLoggedIn()); 972 + // Render emoji picker (enabled only if logged in) 973 + renderEmojiPicker(null, isLoggedIn()); 919 974 920 - // Fetch and render statuses 921 - renderLoading('status-feed'); 922 - try { 923 - const statuses = await fetchStatuses(); 924 - renderStatusFeed(statuses); 925 - } catch (error) { 926 - console.error('Failed to fetch statuses:', error); 927 - document.getElementById('status-feed').innerHTML = ` 975 + // Fetch and render statuses 976 + renderLoading("status-feed"); 977 + try { 978 + const statuses = await fetchStatuses(); 979 + renderStatusFeed(statuses); 980 + } catch (error) { 981 + console.error("Failed to fetch statuses:", error); 982 + document.getElementById("status-feed").innerHTML = ` 928 983 <div class="card"> 929 984 <p class="loading" style="color: var(--error-text);"> 930 - Failed to load statuses. Is the quickslice server running at localhost:8080? 985 + Failed to load statuses. Is the quickslice server running at http://localhost:8080? 931 986 </p> 932 987 </div> 933 988 `; 934 - } 935 - } 989 + } 990 + } 936 991 937 - // Run on page load 938 - main(); 939 - </script> 940 - </body> 992 + // Run on page load 993 + main(); 994 + </script> 995 + </body> 941 996 </html>