slack status without the slack status.zzstoatzz.io/
quickslice

initial commit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+5
.gitignore
···
··· 1 + # local dev files 2 + site/Caddyfile.dev 3 + 4 + # notes 5 + oauth-experience.md
+33
fly.toml
···
··· 1 + # fly.toml app configuration file generated for zzstoatzz-quickslice-status on 2025-12-13T16:42:55-06:00 2 + # 3 + # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 + # 5 + 6 + app = 'zzstoatzz-quickslice-status' 7 + primary_region = 'ewr' 8 + 9 + [build] 10 + image = 'ghcr.io/bigmoves/quickslice:latest' 11 + 12 + [env] 13 + DATABASE_URL = 'sqlite:/data/quickslice.db' 14 + HOST = '0.0.0.0' 15 + PORT = '8080' 16 + EXTERNAL_BASE_URL = 'https://zzstoatzz-quickslice-status.fly.dev' 17 + 18 + [[mounts]] 19 + source = 'quickslice_data' 20 + destination = '/data' 21 + 22 + [http_service] 23 + internal_port = 8080 24 + force_https = true 25 + auto_stop_machines = 'stop' 26 + auto_start_machines = true 27 + min_machines_running = 1 28 + 29 + [[vm]] 30 + memory = '1gb' 31 + cpu_kind = 'shared' 32 + cpus = 1 33 + memory_mb = 1024
lexicons.zip

This is a binary file and will not be displayed.

+30
lexicons/preferences.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.zzstoatzz.status.preferences", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "accentColor": { 12 + "type": "string", 13 + "description": "Hex color for accent/highlight color (e.g. #4a9eff)", 14 + "maxLength": 7 15 + }, 16 + "font": { 17 + "type": "string", 18 + "description": "Font family preference", 19 + "maxLength": 64 20 + }, 21 + "theme": { 22 + "type": "string", 23 + "description": "Theme preference: light, dark, or system", 24 + "enum": ["light", "dark", "system"] 25 + } 26 + } 27 + } 28 + } 29 + } 30 + }
+38
lexicons/status.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "io.zzstoatzz.status.record", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": ["emoji", "createdAt"], 11 + "properties": { 12 + "emoji": { 13 + "type": "string", 14 + "description": "Status emoji or custom emoji slug (e.g. custom:bufo-stab)", 15 + "minLength": 1, 16 + "maxLength": 64 17 + }, 18 + "text": { 19 + "type": "string", 20 + "description": "Optional status text description", 21 + "maxLength": 256, 22 + "maxGraphemes": 256 23 + }, 24 + "expires": { 25 + "type": "string", 26 + "format": "datetime", 27 + "description": "Optional expiration timestamp for this status" 28 + }, 29 + "createdAt": { 30 + "type": "string", 31 + "format": "datetime", 32 + "description": "When this status was created" 33 + } 34 + } 35 + } 36 + } 37 + } 38 + }
+10
site/Caddyfile
···
··· 1 + { 2 + admin off 3 + } 4 + 5 + :8000 { 6 + root * /srv 7 + encode gzip 8 + file_server 9 + try_files {path} /index.html 10 + }
+9
site/Dockerfile
···
··· 1 + FROM caddy:2-alpine 2 + 3 + COPY Caddyfile /etc/caddy/Caddyfile 4 + COPY index.html /srv/index.html 5 + COPY app.js /srv/app.js 6 + COPY styles.css /srv/styles.css 7 + COPY favicon.svg /srv/favicon.svg 8 + 9 + EXPOSE 8000
+1217
site/app.js
···
··· 1 + // Configuration 2 + const CONFIG = { 3 + server: 'https://zzstoatzz-quickslice-status.fly.dev', 4 + clientId: 'client_2mP9AwgVHkg1vaSpcWSsKw', 5 + }; 6 + 7 + let client = null; 8 + let userPreferences = null; 9 + 10 + // Default preferences 11 + const DEFAULT_PREFERENCES = { 12 + accentColor: '#4a9eff', 13 + font: 'mono', 14 + theme: 'dark' 15 + }; 16 + 17 + // Available fonts - use simple keys, map to actual CSS in applyPreferences 18 + const FONTS = [ 19 + { value: 'system', label: 'system' }, 20 + { value: 'mono', label: 'mono' }, 21 + { value: 'serif', label: 'serif' }, 22 + { value: 'comic', label: 'comic' }, 23 + ]; 24 + 25 + const FONT_CSS = { 26 + 'system': 'system-ui, -apple-system, sans-serif', 27 + 'mono': 'ui-monospace, SF Mono, Monaco, monospace', 28 + 'serif': 'ui-serif, Georgia, serif', 29 + 'comic': 'Comic Sans MS, Comic Sans, cursive', 30 + }; 31 + 32 + // Preset accent colors 33 + const ACCENT_COLORS = [ 34 + '#4a9eff', // blue (default) 35 + '#10b981', // green 36 + '#f59e0b', // amber 37 + '#ef4444', // red 38 + '#8b5cf6', // purple 39 + '#ec4899', // pink 40 + '#06b6d4', // cyan 41 + '#f97316', // orange 42 + ]; 43 + 44 + // Apply preferences to the page 45 + function applyPreferences(prefs) { 46 + const { accentColor, font, theme } = { ...DEFAULT_PREFERENCES, ...prefs }; 47 + 48 + document.documentElement.style.setProperty('--accent', accentColor); 49 + // Map simple font key to actual CSS font-family 50 + const fontCSS = FONT_CSS[font] || FONT_CSS['mono']; 51 + document.documentElement.style.setProperty('--font-family', fontCSS); 52 + document.documentElement.setAttribute('data-theme', theme); 53 + 54 + localStorage.setItem('theme', theme); 55 + } 56 + 57 + // Load preferences from server 58 + async function loadPreferences() { 59 + if (!client) return DEFAULT_PREFERENCES; 60 + 61 + try { 62 + const user = client.getUser(); 63 + if (!user) return DEFAULT_PREFERENCES; 64 + 65 + const res = await fetch(`${CONFIG.server}/graphql`, { 66 + method: 'POST', 67 + headers: { 'Content-Type': 'application/json' }, 68 + body: JSON.stringify({ 69 + query: ` 70 + query GetPreferences($did: String!) { 71 + ioZzstoatzzStatusPreferences( 72 + where: { did: { eq: $did } } 73 + first: 1 74 + ) { 75 + edges { node { accentColor font theme } } 76 + } 77 + } 78 + `, 79 + variables: { did: user.did } 80 + }) 81 + }); 82 + const json = await res.json(); 83 + const edges = json.data?.ioZzstoatzzStatusPreferences?.edges || []; 84 + 85 + if (edges.length > 0) { 86 + userPreferences = edges[0].node; 87 + return userPreferences; 88 + } 89 + return DEFAULT_PREFERENCES; 90 + } catch (e) { 91 + console.error('Failed to load preferences:', e); 92 + return DEFAULT_PREFERENCES; 93 + } 94 + } 95 + 96 + // Save preferences to server 97 + async function savePreferences(prefs) { 98 + if (!client) return; 99 + 100 + try { 101 + const user = client.getUser(); 102 + if (!user) return; 103 + 104 + // First, delete any existing preferences records for this user 105 + const res = await fetch(`${CONFIG.server}/graphql`, { 106 + method: 'POST', 107 + headers: { 'Content-Type': 'application/json' }, 108 + body: JSON.stringify({ 109 + query: ` 110 + query GetExistingPrefs($did: String!) { 111 + ioZzstoatzzStatusPreferences(where: { did: { eq: $did } }, first: 50) { 112 + edges { node { uri } } 113 + } 114 + } 115 + `, 116 + variables: { did: user.did } 117 + }) 118 + }); 119 + const json = await res.json(); 120 + const existing = json.data?.ioZzstoatzzStatusPreferences?.edges || []; 121 + 122 + // Delete all existing preference records 123 + for (const edge of existing) { 124 + const rkey = edge.node.uri.split('/').pop(); 125 + try { 126 + await client.mutate(` 127 + mutation DeletePref($rkey: String!) { 128 + deleteIoZzstoatzzStatusPreferences(rkey: $rkey) { uri } 129 + } 130 + `, { rkey }); 131 + } catch (e) { 132 + console.warn('Failed to delete old pref:', e); 133 + } 134 + } 135 + 136 + // Create new preferences record 137 + await client.mutate(` 138 + mutation SavePreferences($input: CreateIoZzstoatzzStatusPreferencesInput!) { 139 + createIoZzstoatzzStatusPreferences(input: $input) { uri } 140 + } 141 + `, { 142 + input: { 143 + accentColor: prefs.accentColor, 144 + font: prefs.font, 145 + theme: prefs.theme 146 + } 147 + }); 148 + 149 + userPreferences = prefs; 150 + applyPreferences(prefs); 151 + } catch (e) { 152 + console.error('Failed to save preferences:', e); 153 + alert('Failed to save preferences: ' + e.message); 154 + } 155 + } 156 + 157 + // Create settings modal 158 + function createSettingsModal() { 159 + const overlay = document.createElement('div'); 160 + overlay.className = 'settings-overlay hidden'; 161 + overlay.innerHTML = ` 162 + <div class="settings-modal"> 163 + <div class="settings-header"> 164 + <h3>settings</h3> 165 + <button class="settings-close" aria-label="close">✕</button> 166 + </div> 167 + <div class="settings-content"> 168 + <div class="setting-group"> 169 + <label>accent color</label> 170 + <div class="color-picker"> 171 + ${ACCENT_COLORS.map(c => ` 172 + <button class="color-btn" data-color="${c}" style="background: ${c}" title="${c}"></button> 173 + `).join('')} 174 + <input type="color" id="custom-color" class="custom-color-input" title="custom color"> 175 + </div> 176 + </div> 177 + <div class="setting-group"> 178 + <label>font</label> 179 + <select id="font-select"> 180 + ${FONTS.map(f => `<option value="${f.value}">${f.label}</option>`).join('')} 181 + </select> 182 + </div> 183 + <div class="setting-group"> 184 + <label>theme</label> 185 + <select id="theme-select"> 186 + <option value="dark">dark</option> 187 + <option value="light">light</option> 188 + <option value="system">system</option> 189 + </select> 190 + </div> 191 + </div> 192 + <div class="settings-footer"> 193 + <button id="save-settings" class="save-btn">save</button> 194 + </div> 195 + </div> 196 + `; 197 + 198 + const modal = overlay.querySelector('.settings-modal'); 199 + const closeBtn = overlay.querySelector('.settings-close'); 200 + const colorBtns = overlay.querySelectorAll('.color-btn'); 201 + const customColor = overlay.querySelector('#custom-color'); 202 + const fontSelect = overlay.querySelector('#font-select'); 203 + const themeSelect = overlay.querySelector('#theme-select'); 204 + const saveBtn = overlay.querySelector('#save-settings'); 205 + 206 + let currentPrefs = { ...DEFAULT_PREFERENCES }; 207 + 208 + function updateColorSelection(color) { 209 + colorBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.color === color)); 210 + customColor.value = color; 211 + currentPrefs.accentColor = color; 212 + } 213 + 214 + function open(prefs) { 215 + currentPrefs = { ...DEFAULT_PREFERENCES, ...prefs }; 216 + updateColorSelection(currentPrefs.accentColor); 217 + fontSelect.value = currentPrefs.font; 218 + themeSelect.value = currentPrefs.theme; 219 + overlay.classList.remove('hidden'); 220 + } 221 + 222 + function close() { 223 + overlay.classList.add('hidden'); 224 + } 225 + 226 + overlay.addEventListener('click', e => { if (e.target === overlay) close(); }); 227 + closeBtn.addEventListener('click', close); 228 + 229 + colorBtns.forEach(btn => { 230 + btn.addEventListener('click', () => updateColorSelection(btn.dataset.color)); 231 + }); 232 + 233 + customColor.addEventListener('input', () => { 234 + updateColorSelection(customColor.value); 235 + }); 236 + 237 + fontSelect.addEventListener('change', () => { 238 + currentPrefs.font = fontSelect.value; 239 + }); 240 + 241 + themeSelect.addEventListener('change', () => { 242 + currentPrefs.theme = themeSelect.value; 243 + }); 244 + 245 + saveBtn.addEventListener('click', async () => { 246 + saveBtn.disabled = true; 247 + saveBtn.textContent = 'saving...'; 248 + await savePreferences(currentPrefs); 249 + saveBtn.disabled = false; 250 + saveBtn.textContent = 'save'; 251 + close(); 252 + }); 253 + 254 + document.body.appendChild(overlay); 255 + return { open, close }; 256 + } 257 + 258 + // Theme (fallback for non-logged-in users) 259 + function initTheme() { 260 + const saved = localStorage.getItem('theme') || 'dark'; 261 + document.documentElement.setAttribute('data-theme', saved); 262 + } 263 + 264 + function toggleTheme() { 265 + const current = document.documentElement.getAttribute('data-theme'); 266 + const next = current === 'dark' ? 'light' : 'dark'; 267 + document.documentElement.setAttribute('data-theme', next); 268 + localStorage.setItem('theme', next); 269 + 270 + // If logged in, also update preferences 271 + if (userPreferences) { 272 + userPreferences.theme = next; 273 + savePreferences(userPreferences); 274 + } 275 + } 276 + 277 + // Timestamp formatting (ported from original status app) 278 + const TimestampFormatter = { 279 + formatRelative(date, now = new Date()) { 280 + const diffMs = now - date; 281 + const diffMins = Math.floor(diffMs / 60000); 282 + const diffHours = Math.floor(diffMs / 3600000); 283 + const diffDays = Math.floor(diffMs / 86400000); 284 + 285 + if (diffMs < 30000) return 'just now'; 286 + if (diffMins < 60) return `${diffMins}m ago`; 287 + if (diffHours < 24) { 288 + const remainingMins = diffMins % 60; 289 + return remainingMins === 0 ? `${diffHours}h ago` : `${diffHours}h ${remainingMins}m ago`; 290 + } 291 + if (diffDays < 7) { 292 + const remainingHours = diffHours % 24; 293 + return remainingHours === 0 ? `${diffDays}d ago` : `${diffDays}d ${remainingHours}h ago`; 294 + } 295 + 296 + const timeStr = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 297 + if (date.getFullYear() === now.getFullYear()) { 298 + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' + timeStr; 299 + } 300 + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + ', ' + timeStr; 301 + }, 302 + 303 + formatCompact(date, now = new Date()) { 304 + const diffMs = now - date; 305 + const diffDays = Math.floor(diffMs / 86400000); 306 + 307 + if (date.toDateString() === now.toDateString()) { 308 + return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 309 + } 310 + const yesterday = new Date(now); 311 + yesterday.setDate(yesterday.getDate() - 1); 312 + if (date.toDateString() === yesterday.toDateString()) { 313 + return 'yesterday, ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 314 + } 315 + if (diffDays < 7) { 316 + const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase(); 317 + const time = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 318 + return `${dayName}, ${time}`; 319 + } 320 + if (date.getFullYear() === now.getFullYear()) { 321 + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 322 + } 323 + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 324 + }, 325 + 326 + getFullTimestamp(date) { 327 + const dayName = date.toLocaleDateString('en-US', { weekday: 'long' }); 328 + const monthDay = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); 329 + const time = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true }); 330 + const tzAbbr = date.toLocaleTimeString('en-US', { timeZoneName: 'short' }).split(' ').pop(); 331 + return `${dayName}, ${monthDay} at ${time} ${tzAbbr}`; 332 + } 333 + }; 334 + 335 + function relativeTime(dateStr, format = 'relative') { 336 + const date = new Date(dateStr); 337 + return format === 'compact' 338 + ? TimestampFormatter.formatCompact(date) 339 + : TimestampFormatter.formatRelative(date); 340 + } 341 + 342 + function relativeTimeFuture(dateStr) { 343 + const date = new Date(dateStr); 344 + const now = new Date(); 345 + const diffMs = date - now; 346 + 347 + if (diffMs <= 0) return 'now'; 348 + 349 + const diffMins = Math.floor(diffMs / 60000); 350 + const diffHours = Math.floor(diffMs / 3600000); 351 + const diffDays = Math.floor(diffMs / 86400000); 352 + 353 + if (diffMins < 1) return 'in less than a minute'; 354 + if (diffMins < 60) return `in ${diffMins}m`; 355 + if (diffHours < 24) { 356 + const remainingMins = diffMins % 60; 357 + return remainingMins === 0 ? `in ${diffHours}h` : `in ${diffHours}h ${remainingMins}m`; 358 + } 359 + if (diffDays < 7) { 360 + const remainingHours = diffHours % 24; 361 + return remainingHours === 0 ? `in ${diffDays}d` : `in ${diffDays}d ${remainingHours}h`; 362 + } 363 + 364 + // For longer times, show the date 365 + const timeStr = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase(); 366 + if (date.getFullYear() === now.getFullYear()) { 367 + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' + timeStr; 368 + } 369 + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + ', ' + timeStr; 370 + } 371 + 372 + function fullTimestamp(dateStr) { 373 + return TimestampFormatter.getFullTimestamp(new Date(dateStr)); 374 + } 375 + 376 + // Emoji picker 377 + let emojiData = null; 378 + let bufoList = null; 379 + let userFrequentEmojis = null; 380 + const DEFAULT_FREQUENT_EMOJIS = ['😊', '👍', '❤️', '😂', '🎉', '🔥', '✨', '💯', '🚀', '💪', '🙏', '👏', '😴', '🤔', '👀', '💻']; 381 + 382 + async function loadUserFrequentEmojis() { 383 + if (userFrequentEmojis) return userFrequentEmojis; 384 + if (!client) return DEFAULT_FREQUENT_EMOJIS; 385 + 386 + try { 387 + const user = client.getUser(); 388 + if (!user) return DEFAULT_FREQUENT_EMOJIS; 389 + 390 + // Fetch user's status history to count emoji usage 391 + const res = await fetch(`${CONFIG.server}/graphql`, { 392 + method: 'POST', 393 + headers: { 'Content-Type': 'application/json' }, 394 + body: JSON.stringify({ 395 + query: ` 396 + query GetUserEmojis($did: String!) { 397 + ioZzstoatzzStatusRecord( 398 + first: 100 399 + where: { did: { eq: $did } } 400 + ) { 401 + edges { node { emoji } } 402 + } 403 + } 404 + `, 405 + variables: { did: user.did } 406 + }) 407 + }); 408 + const json = await res.json(); 409 + const emojis = json.data?.ioZzstoatzzStatusRecord?.edges?.map(e => e.node.emoji) || []; 410 + 411 + if (emojis.length === 0) return DEFAULT_FREQUENT_EMOJIS; 412 + 413 + // Count emoji frequency 414 + const counts = {}; 415 + emojis.forEach(e => { counts[e] = (counts[e] || 0) + 1; }); 416 + 417 + // Sort by frequency and take top 16 418 + const sorted = Object.entries(counts) 419 + .sort((a, b) => b[1] - a[1]) 420 + .slice(0, 16) 421 + .map(([emoji]) => emoji); 422 + 423 + userFrequentEmojis = sorted.length > 0 ? sorted : DEFAULT_FREQUENT_EMOJIS; 424 + return userFrequentEmojis; 425 + } catch (e) { 426 + console.error('Failed to load frequent emojis:', e); 427 + return DEFAULT_FREQUENT_EMOJIS; 428 + } 429 + } 430 + 431 + async function loadBufoList() { 432 + if (bufoList) return bufoList; 433 + const res = await fetch('/bufos.json'); 434 + if (!res.ok) throw new Error('Failed to load bufos'); 435 + bufoList = await res.json(); 436 + return bufoList; 437 + } 438 + 439 + async function loadEmojiData() { 440 + if (emojiData) return emojiData; 441 + try { 442 + const response = await fetch('https://cdn.jsdelivr.net/npm/emoji-datasource@15.1.0/emoji.json'); 443 + if (!response.ok) throw new Error('Failed to fetch'); 444 + const data = await response.json(); 445 + 446 + const emojis = {}; 447 + const categories = { frequent: DEFAULT_FREQUENT_EMOJIS, people: [], nature: [], food: [], activity: [], travel: [], objects: [], symbols: [], flags: [] }; 448 + const categoryMap = { 449 + 'Smileys & Emotion': 'people', 'People & Body': 'people', 'Animals & Nature': 'nature', 450 + 'Food & Drink': 'food', 'Activities': 'activity', 'Travel & Places': 'travel', 451 + 'Objects': 'objects', 'Symbols': 'symbols', 'Flags': 'flags' 452 + }; 453 + 454 + data.forEach(emoji => { 455 + const char = emoji.unified.split('-').map(u => String.fromCodePoint(parseInt(u, 16))).join(''); 456 + const keywords = [...(emoji.short_names || []), ...(emoji.name ? emoji.name.toLowerCase().split(/[\s_-]+/) : [])]; 457 + emojis[char] = keywords; 458 + const cat = categoryMap[emoji.category]; 459 + if (cat && categories[cat]) categories[cat].push(char); 460 + }); 461 + 462 + emojiData = { emojis, categories }; 463 + return emojiData; 464 + } catch (e) { 465 + console.error('Failed to load emoji data:', e); 466 + return { emojis: {}, categories: { frequent: DEFAULT_FREQUENT_EMOJIS, people: [], nature: [], food: [], activity: [], travel: [], objects: [], symbols: [], flags: [] } }; 467 + } 468 + } 469 + 470 + function searchEmojis(query, data) { 471 + if (!query) return []; 472 + const q = query.toLowerCase(); 473 + return Object.entries(data.emojis) 474 + .filter(([char, keywords]) => keywords.some(k => k.includes(q))) 475 + .map(([char]) => char) 476 + .slice(0, 50); 477 + } 478 + 479 + function createEmojiPicker(onSelect) { 480 + const overlay = document.createElement('div'); 481 + overlay.className = 'emoji-picker-overlay hidden'; 482 + overlay.innerHTML = ` 483 + <div class="emoji-picker"> 484 + <div class="emoji-picker-header"> 485 + <h3>pick an emoji</h3> 486 + <button class="emoji-picker-close" aria-label="close">✕</button> 487 + </div> 488 + <input type="text" class="emoji-search" placeholder="search emojis..."> 489 + <div class="emoji-categories"> 490 + <button class="category-btn active" data-category="frequent">⭐</button> 491 + <button class="category-btn" data-category="custom">🐸</button> 492 + <button class="category-btn" data-category="people">😊</button> 493 + <button class="category-btn" data-category="nature">🌿</button> 494 + <button class="category-btn" data-category="food">🍔</button> 495 + <button class="category-btn" data-category="activity">⚽</button> 496 + <button class="category-btn" data-category="travel">✈️</button> 497 + <button class="category-btn" data-category="objects">💡</button> 498 + <button class="category-btn" data-category="symbols">💕</button> 499 + <button class="category-btn" data-category="flags">🏁</button> 500 + </div> 501 + <div class="emoji-grid"></div> 502 + <div class="bufo-helper hidden"><a href="https://find-bufo.fly.dev/" target="_blank">need help finding a bufo?</a></div> 503 + </div> 504 + `; 505 + 506 + const picker = overlay.querySelector('.emoji-picker'); 507 + const grid = overlay.querySelector('.emoji-grid'); 508 + const search = overlay.querySelector('.emoji-search'); 509 + const closeBtn = overlay.querySelector('.emoji-picker-close'); 510 + const categoryBtns = overlay.querySelectorAll('.category-btn'); 511 + const bufoHelper = overlay.querySelector('.bufo-helper'); 512 + 513 + let currentCategory = 'frequent'; 514 + let data = null; 515 + 516 + async function renderCategory(cat) { 517 + currentCategory = cat; 518 + categoryBtns.forEach(b => b.classList.toggle('active', b.dataset.category === cat)); 519 + bufoHelper.classList.toggle('hidden', cat !== 'custom'); 520 + 521 + if (cat === 'custom') { 522 + grid.classList.add('bufo-grid'); 523 + grid.innerHTML = '<div class="loading">loading bufos...</div>'; 524 + try { 525 + const bufos = await loadBufoList(); 526 + grid.innerHTML = bufos.map(name => ` 527 + <button class="emoji-btn bufo-btn" data-emoji="custom:${name}" title="${name}"> 528 + <img src="https://all-the.bufo.zone/${name}.png" alt="${name}" loading="lazy" onerror="this.src='https://all-the.bufo.zone/${name}.gif'"> 529 + </button> 530 + `).join(''); 531 + } catch (e) { 532 + grid.innerHTML = '<div class="no-results">failed to load bufos</div>'; 533 + } 534 + return; 535 + } 536 + 537 + grid.classList.remove('bufo-grid'); 538 + 539 + // Load user's frequent emojis for the frequent category 540 + if (cat === 'frequent') { 541 + grid.innerHTML = '<div class="loading">loading...</div>'; 542 + const frequentEmojis = await loadUserFrequentEmojis(); 543 + grid.innerHTML = frequentEmojis.map(e => { 544 + if (e.startsWith('custom:')) { 545 + const name = e.replace('custom:', ''); 546 + return `<button class="emoji-btn bufo-btn" data-emoji="${e}" title="${name}"> 547 + <img src="https://all-the.bufo.zone/${name}.png" alt="${name}" onerror="this.src='https://all-the.bufo.zone/${name}.gif'"> 548 + </button>`; 549 + } 550 + return `<button class="emoji-btn" data-emoji="${e}">${e}</button>`; 551 + }).join(''); 552 + return; 553 + } 554 + 555 + if (!data) data = await loadEmojiData(); 556 + const emojis = data.categories[cat] || []; 557 + grid.innerHTML = emojis.map(e => `<button class="emoji-btn" data-emoji="${e}">${e}</button>`).join(''); 558 + } 559 + 560 + function close() { 561 + overlay.classList.add('hidden'); 562 + search.value = ''; 563 + } 564 + 565 + function open() { 566 + overlay.classList.remove('hidden'); 567 + renderCategory('frequent'); 568 + search.focus(); 569 + } 570 + 571 + overlay.addEventListener('click', e => { if (e.target === overlay) close(); }); 572 + closeBtn.addEventListener('click', close); 573 + categoryBtns.forEach(btn => btn.addEventListener('click', () => renderCategory(btn.dataset.category))); 574 + 575 + grid.addEventListener('click', e => { 576 + const btn = e.target.closest('.emoji-btn'); 577 + if (btn) { 578 + onSelect(btn.dataset.emoji); 579 + close(); 580 + } 581 + }); 582 + 583 + search.addEventListener('input', async () => { 584 + const q = search.value.trim(); 585 + if (!q) { renderCategory(currentCategory); return; } 586 + 587 + // Search both emojis and bufos 588 + if (!data) data = await loadEmojiData(); 589 + const emojiResults = searchEmojis(q, data); 590 + 591 + // Search bufos by name 592 + let bufoResults = []; 593 + try { 594 + const bufos = await loadBufoList(); 595 + const qLower = q.toLowerCase(); 596 + bufoResults = bufos.filter(name => name.toLowerCase().includes(qLower)).slice(0, 30); 597 + } catch (e) { /* ignore */ } 598 + 599 + grid.classList.remove('bufo-grid'); 600 + bufoHelper.classList.add('hidden'); 601 + 602 + if (emojiResults.length === 0 && bufoResults.length === 0) { 603 + grid.innerHTML = '<div class="no-results">no emojis found</div>'; 604 + return; 605 + } 606 + 607 + let html = ''; 608 + // Show emoji results first 609 + html += emojiResults.map(e => `<button class="emoji-btn" data-emoji="${e}">${e}</button>`).join(''); 610 + // Then bufo results 611 + html += bufoResults.map(name => ` 612 + <button class="emoji-btn bufo-btn" data-emoji="custom:${name}" title="${name}"> 613 + <img src="https://all-the.bufo.zone/${name}.png" alt="${name}" onerror="this.src='https://all-the.bufo.zone/${name}.gif'"> 614 + </button> 615 + `).join(''); 616 + 617 + grid.innerHTML = html; 618 + }); 619 + 620 + document.body.appendChild(overlay); 621 + return { open, close }; 622 + } 623 + 624 + // Render emoji (handles custom:name format) 625 + function renderEmoji(emoji) { 626 + if (emoji && emoji.startsWith('custom:')) { 627 + const name = emoji.slice(7); 628 + return `<img src="https://all-the.bufo.zone/${name}.png" alt="${name}" title="${name}" onerror="this.src='https://all-the.bufo.zone/${name}.gif'">`; 629 + } 630 + return emoji || '-'; 631 + } 632 + 633 + function escapeHtml(str) { 634 + if (!str) return ''; 635 + const div = document.createElement('div'); 636 + div.textContent = str; 637 + return div.innerHTML; 638 + } 639 + 640 + // Parse markdown links [text](url) and return HTML 641 + function parseLinks(text) { 642 + if (!text) return ''; 643 + // First escape HTML, then parse markdown links 644 + const escaped = escapeHtml(text); 645 + // Match [text](url) pattern 646 + return escaped.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, linkText, url) => { 647 + // Validate URL (basic check) 648 + if (url.startsWith('http://') || url.startsWith('https://')) { 649 + return `<a href="${url}" target="_blank" rel="noopener">${linkText}</a>`; 650 + } 651 + return match; 652 + }); 653 + } 654 + 655 + // Resolve handle to DID 656 + async function resolveHandle(handle) { 657 + const res = await fetch(`https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); 658 + if (!res.ok) return null; 659 + const data = await res.json(); 660 + return data.did; 661 + } 662 + 663 + // Resolve DID to handle 664 + async function resolveDidToHandle(did) { 665 + const res = await fetch(`https://plc.directory/${did}`); 666 + if (!res.ok) return null; 667 + const data = await res.json(); 668 + // alsoKnownAs is like ["at://handle"] 669 + if (data.alsoKnownAs && data.alsoKnownAs.length > 0) { 670 + return data.alsoKnownAs[0].replace('at://', ''); 671 + } 672 + return null; 673 + } 674 + 675 + // Router 676 + function getRoute() { 677 + const path = window.location.pathname; 678 + if (path === '/' || path === '/index.html') return { page: 'home' }; 679 + if (path === '/feed' || path === '/feed.html') return { page: 'feed' }; 680 + if (path.startsWith('/@')) { 681 + const handle = path.slice(2); 682 + return { page: 'profile', handle }; 683 + } 684 + return { page: '404' }; 685 + } 686 + 687 + // Render home page 688 + async function renderHome() { 689 + const main = document.getElementById('main-content'); 690 + document.getElementById('page-title').textContent = 'status'; 691 + 692 + if (typeof QuicksliceClient === 'undefined') { 693 + main.innerHTML = '<div class="center">failed to load. check console.</div>'; 694 + return; 695 + } 696 + 697 + try { 698 + client = await QuicksliceClient.createQuicksliceClient({ 699 + server: CONFIG.server, 700 + clientId: CONFIG.clientId, 701 + redirectUri: window.location.origin + '/', 702 + }); 703 + console.log('Client created with server:', CONFIG.server, 'clientId:', CONFIG.clientId); 704 + 705 + if (window.location.search.includes('code=')) { 706 + console.log('Got OAuth callback with code, handling...'); 707 + try { 708 + const result = await client.handleRedirectCallback(); 709 + console.log('handleRedirectCallback result:', result); 710 + } catch (err) { 711 + console.error('handleRedirectCallback error:', err); 712 + } 713 + window.history.replaceState({}, document.title, '/'); 714 + } 715 + 716 + const isAuthed = await client.isAuthenticated(); 717 + 718 + if (!isAuthed) { 719 + main.innerHTML = ` 720 + <div class="center"> 721 + <p>share your status on the atproto network</p> 722 + <form id="login-form"> 723 + <input type="text" id="handle-input" placeholder="your.handle" required> 724 + <button type="submit">log in</button> 725 + </form> 726 + </div> 727 + `; 728 + document.getElementById('login-form').addEventListener('submit', async (e) => { 729 + e.preventDefault(); 730 + const handle = document.getElementById('handle-input').value.trim(); 731 + if (handle && client) { 732 + await client.loginWithRedirect({ handle }); 733 + } 734 + }); 735 + } else { 736 + const user = client.getUser(); 737 + if (!user) { 738 + // Token might be invalid, log out 739 + await client.logout(); 740 + window.location.reload(); 741 + return; 742 + } 743 + const handle = await resolveDidToHandle(user.did) || user.did; 744 + 745 + // Load and apply preferences, set up settings/logout buttons 746 + const prefs = await loadPreferences(); 747 + applyPreferences(prefs); 748 + 749 + // Show settings button and set up modal 750 + const settingsBtn = document.getElementById('settings-btn'); 751 + settingsBtn.classList.remove('hidden'); 752 + const settingsModal = createSettingsModal(); 753 + settingsBtn.addEventListener('click', () => settingsModal.open(userPreferences || prefs)); 754 + 755 + // Add logout button to header nav (if not already there) 756 + if (!document.getElementById('logout-btn')) { 757 + const nav = document.querySelector('header nav'); 758 + const logoutBtn = document.createElement('button'); 759 + logoutBtn.id = 'logout-btn'; 760 + logoutBtn.className = 'nav-btn'; 761 + logoutBtn.setAttribute('aria-label', 'log out'); 762 + logoutBtn.setAttribute('title', 'log out'); 763 + logoutBtn.innerHTML = ` 764 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 765 + <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path> 766 + <polyline points="16 17 21 12 16 7"></polyline> 767 + <line x1="21" y1="12" x2="9" y2="12"></line> 768 + </svg> 769 + `; 770 + logoutBtn.addEventListener('click', async () => { 771 + await client.logout(); 772 + window.location.href = '/'; 773 + }); 774 + nav.appendChild(logoutBtn); 775 + } 776 + 777 + // Set page title with Bluesky profile link 778 + document.getElementById('page-title').innerHTML = `<a href="https://bsky.app/profile/${handle}" target="_blank">@${handle}</a>`; 779 + 780 + // Load user's statuses (full history) 781 + const res = await fetch(`${CONFIG.server}/graphql`, { 782 + method: 'POST', 783 + headers: { 'Content-Type': 'application/json' }, 784 + body: JSON.stringify({ 785 + query: ` 786 + query GetUserStatuses($did: String!) { 787 + ioZzstoatzzStatusRecord( 788 + first: 100 789 + where: { did: { eq: $did } } 790 + sortBy: [{ field: "createdAt", direction: DESC }] 791 + ) { 792 + edges { node { uri did emoji text createdAt expires } } 793 + } 794 + } 795 + `, 796 + variables: { did: user.did } 797 + }) 798 + }); 799 + const json = await res.json(); 800 + const statuses = json.data.ioZzstoatzzStatusRecord.edges.map(e => e.node); 801 + 802 + let currentHtml = '<span class="big-emoji">-</span>'; 803 + let historyHtml = ''; 804 + 805 + if (statuses.length > 0) { 806 + const current = statuses[0]; 807 + const expiresHtml = current.expires ? ` • clears ${relativeTimeFuture(current.expires)}` : ''; 808 + currentHtml = ` 809 + <span class="big-emoji">${renderEmoji(current.emoji)}</span> 810 + <div class="status-info"> 811 + ${current.text ? `<span id="current-text">${parseLinks(current.text)}</span>` : ''} 812 + <span class="meta">since ${relativeTime(current.createdAt)}${expiresHtml}</span> 813 + </div> 814 + `; 815 + if (statuses.length > 1) { 816 + historyHtml = '<section class="history"><h2>history</h2><div id="history-list">'; 817 + statuses.slice(1).forEach(s => { 818 + // Extract rkey from URI (at://did/collection/rkey) 819 + const rkey = s.uri.split('/').pop(); 820 + historyHtml += ` 821 + <div class="status-item"> 822 + <span class="emoji">${renderEmoji(s.emoji)}</span> 823 + <div class="content"> 824 + <div>${s.text ? `<span class="text">${parseLinks(s.text)}</span>` : ''}</div> 825 + <span class="time">${relativeTime(s.createdAt)}</span> 826 + </div> 827 + <button class="delete-btn" data-rkey="${escapeHtml(rkey)}" title="delete"> 828 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 829 + <line x1="18" y1="6" x2="6" y2="18"></line> 830 + <line x1="6" y1="6" x2="18" y2="18"></line> 831 + </svg> 832 + </button> 833 + </div> 834 + `; 835 + }); 836 + historyHtml += '</div></section>'; 837 + } 838 + } 839 + 840 + const currentEmoji = statuses.length > 0 ? statuses[0].emoji : '😊'; 841 + 842 + main.innerHTML = ` 843 + <div class="profile-card"> 844 + <div class="current-status">${currentHtml}</div> 845 + </div> 846 + <form id="status-form" class="status-form"> 847 + <div class="emoji-input-row"> 848 + <button type="button" id="emoji-trigger" class="emoji-trigger"> 849 + <span id="selected-emoji">${renderEmoji(currentEmoji)}</span> 850 + </button> 851 + <input type="hidden" id="emoji-input" value="${escapeHtml(currentEmoji)}"> 852 + <input type="text" id="text-input" placeholder="what's happening?" maxlength="256"> 853 + </div> 854 + <div class="form-actions"> 855 + <select id="expires-select"> 856 + <option value="">don't clear</option> 857 + <option value="30">30 min</option> 858 + <option value="60">1 hour</option> 859 + <option value="120">2 hours</option> 860 + <option value="240">4 hours</option> 861 + <option value="480">8 hours</option> 862 + <option value="1440">1 day</option> 863 + <option value="10080">1 week</option> 864 + <option value="custom">custom...</option> 865 + </select> 866 + <input type="datetime-local" id="custom-datetime" class="custom-datetime hidden"> 867 + <button type="submit">set status</button> 868 + </div> 869 + </form> 870 + ${historyHtml} 871 + `; 872 + 873 + // Set up emoji picker 874 + const emojiInput = document.getElementById('emoji-input'); 875 + const selectedEmojiEl = document.getElementById('selected-emoji'); 876 + const emojiPicker = createEmojiPicker((emoji) => { 877 + emojiInput.value = emoji; 878 + selectedEmojiEl.innerHTML = renderEmoji(emoji); 879 + }); 880 + document.getElementById('emoji-trigger').addEventListener('click', () => emojiPicker.open()); 881 + 882 + // Custom datetime toggle 883 + const expiresSelect = document.getElementById('expires-select'); 884 + const customDatetime = document.getElementById('custom-datetime'); 885 + 886 + // Helper to format date for datetime-local input (local timezone) 887 + function toLocalDatetimeString(date) { 888 + const offset = date.getTimezoneOffset(); 889 + const local = new Date(date.getTime() - offset * 60 * 1000); 890 + return local.toISOString().slice(0, 16); 891 + } 892 + 893 + expiresSelect.addEventListener('change', () => { 894 + if (expiresSelect.value === 'custom') { 895 + customDatetime.classList.remove('hidden'); 896 + // Set min to now (prevent past dates) 897 + const now = new Date(); 898 + customDatetime.min = toLocalDatetimeString(now); 899 + // Default to 1 hour from now 900 + const defaultTime = new Date(Date.now() + 60 * 60 * 1000); 901 + customDatetime.value = toLocalDatetimeString(defaultTime); 902 + } else { 903 + customDatetime.classList.add('hidden'); 904 + } 905 + }); 906 + 907 + document.getElementById('status-form').addEventListener('submit', async (e) => { 908 + e.preventDefault(); 909 + const emoji = document.getElementById('emoji-input').value.trim(); 910 + const text = document.getElementById('text-input').value.trim(); 911 + const expiresVal = document.getElementById('expires-select').value; 912 + const customDt = document.getElementById('custom-datetime').value; 913 + 914 + if (!emoji) return; 915 + 916 + const input = { emoji, createdAt: new Date().toISOString() }; 917 + if (text) input.text = text; 918 + if (expiresVal === 'custom' && customDt) { 919 + input.expires = new Date(customDt).toISOString(); 920 + } else if (expiresVal && expiresVal !== 'custom') { 921 + input.expires = new Date(Date.now() + parseInt(expiresVal) * 60 * 1000).toISOString(); 922 + } 923 + 924 + try { 925 + await client.mutate(` 926 + mutation CreateStatus($input: CreateIoZzstoatzzStatusRecordInput!) { 927 + createIoZzstoatzzStatusRecord(input: $input) { uri } 928 + } 929 + `, { input }); 930 + window.location.reload(); 931 + } catch (err) { 932 + console.error('Failed to create status:', err); 933 + alert('Failed to set status: ' + err.message); 934 + } 935 + }); 936 + 937 + // Delete buttons 938 + document.querySelectorAll('.delete-btn').forEach(btn => { 939 + btn.addEventListener('click', async () => { 940 + const rkey = btn.dataset.rkey; 941 + if (!confirm('Delete this status?')) return; 942 + 943 + try { 944 + await client.mutate(` 945 + mutation DeleteStatus($rkey: String!) { 946 + deleteIoZzstoatzzStatusRecord(rkey: $rkey) { uri } 947 + } 948 + `, { rkey }); 949 + window.location.reload(); 950 + } catch (err) { 951 + console.error('Failed to delete status:', err); 952 + alert('Failed to delete: ' + err.message); 953 + } 954 + }); 955 + }); 956 + } 957 + } catch (e) { 958 + console.error('Failed to init:', e); 959 + main.innerHTML = '<div class="center">failed to initialize. check console.</div>'; 960 + } 961 + } 962 + 963 + // Render feed page 964 + let feedCursor = null; 965 + let feedHasMore = true; 966 + 967 + async function renderFeed(append = false) { 968 + const main = document.getElementById('main-content'); 969 + document.getElementById('page-title').textContent = 'global feed'; 970 + 971 + if (!append) { 972 + // Initialize auth UI for header elements 973 + await initAuthUI(); 974 + main.innerHTML = '<div id="feed-list" class="feed-list"><div class="center">loading...</div></div><div id="load-more" class="center hidden"><button id="load-more-btn">load more</button></div><div id="end-of-feed" class="center hidden"><span class="meta">you\'ve reached the end</span></div>'; 975 + } 976 + 977 + const feedList = document.getElementById('feed-list'); 978 + 979 + try { 980 + const res = await fetch(`${CONFIG.server}/graphql`, { 981 + method: 'POST', 982 + headers: { 'Content-Type': 'application/json' }, 983 + body: JSON.stringify({ 984 + query: ` 985 + query GetFeed($after: String) { 986 + ioZzstoatzzStatusRecord(first: 20, after: $after, sortBy: [{ field: "createdAt", direction: DESC }]) { 987 + edges { node { uri did emoji text createdAt } cursor } 988 + pageInfo { hasNextPage endCursor } 989 + } 990 + } 991 + `, 992 + variables: { after: append ? feedCursor : null } 993 + }) 994 + }); 995 + 996 + const json = await res.json(); 997 + const data = json.data.ioZzstoatzzStatusRecord; 998 + const statuses = data.edges.map(e => e.node); 999 + feedCursor = data.pageInfo.endCursor; 1000 + feedHasMore = data.pageInfo.hasNextPage; 1001 + 1002 + // Resolve all handles in parallel 1003 + const handlePromises = statuses.map(s => resolveDidToHandle(s.did)); 1004 + const handles = await Promise.all(handlePromises); 1005 + 1006 + if (!append) { 1007 + feedList.innerHTML = ''; 1008 + } 1009 + 1010 + statuses.forEach((status, i) => { 1011 + const handle = handles[i] || status.did.slice(8, 28); 1012 + const div = document.createElement('div'); 1013 + div.className = 'status-item'; 1014 + div.innerHTML = ` 1015 + <span class="emoji">${renderEmoji(status.emoji)}</span> 1016 + <div class="content"> 1017 + <div> 1018 + <a href="/@${handle}" class="author">@${handle}</a> 1019 + ${status.text ? `<span class="text">${parseLinks(status.text)}</span>` : ''} 1020 + </div> 1021 + <span class="time">${relativeTime(status.createdAt)}</span> 1022 + </div> 1023 + `; 1024 + feedList.appendChild(div); 1025 + }); 1026 + 1027 + const loadMore = document.getElementById('load-more'); 1028 + const endOfFeed = document.getElementById('end-of-feed'); 1029 + if (feedHasMore) { 1030 + loadMore.classList.remove('hidden'); 1031 + endOfFeed.classList.add('hidden'); 1032 + } else { 1033 + loadMore.classList.add('hidden'); 1034 + endOfFeed.classList.remove('hidden'); 1035 + } 1036 + 1037 + // Attach load more handler 1038 + const btn = document.getElementById('load-more-btn'); 1039 + if (btn && !btn.dataset.bound) { 1040 + btn.dataset.bound = 'true'; 1041 + btn.addEventListener('click', () => renderFeed(true)); 1042 + } 1043 + } catch (e) { 1044 + console.error('Failed to load feed:', e); 1045 + if (!append) { 1046 + feedList.innerHTML = '<div class="center">failed to load feed</div>'; 1047 + } 1048 + } 1049 + } 1050 + 1051 + // Render profile page 1052 + async function renderProfile(handle) { 1053 + const main = document.getElementById('main-content'); 1054 + const pageTitle = document.getElementById('page-title'); 1055 + 1056 + // Initialize auth UI for header elements 1057 + await initAuthUI(); 1058 + 1059 + pageTitle.innerHTML = `<a href="https://bsky.app/profile/${handle}" target="_blank">@${handle}</a>`; 1060 + 1061 + main.innerHTML = '<div class="center">loading...</div>'; 1062 + 1063 + try { 1064 + // Resolve handle to DID 1065 + const did = await resolveHandle(handle); 1066 + if (!did) { 1067 + main.innerHTML = '<div class="center">user not found</div>'; 1068 + return; 1069 + } 1070 + 1071 + const res = await fetch(`${CONFIG.server}/graphql`, { 1072 + method: 'POST', 1073 + headers: { 'Content-Type': 'application/json' }, 1074 + body: JSON.stringify({ 1075 + query: ` 1076 + query GetUserStatuses($did: String!) { 1077 + ioZzstoatzzStatusRecord(first: 20, where: { did: { eq: $did } }, sortBy: [{ field: "createdAt", direction: DESC }]) { 1078 + edges { node { uri did emoji text createdAt expires } } 1079 + } 1080 + } 1081 + `, 1082 + variables: { did } 1083 + }) 1084 + }); 1085 + 1086 + const json = await res.json(); 1087 + const statuses = json.data.ioZzstoatzzStatusRecord.edges.map(e => e.node); 1088 + 1089 + if (statuses.length === 0) { 1090 + main.innerHTML = '<div class="center">no statuses yet</div>'; 1091 + return; 1092 + } 1093 + 1094 + const current = statuses[0]; 1095 + const expiresHtml = current.expires ? ` • clears ${relativeTimeFuture(current.expires)}` : ''; 1096 + let html = ` 1097 + <div class="profile-card"> 1098 + <div class="current-status"> 1099 + <span class="big-emoji">${renderEmoji(current.emoji)}</span> 1100 + <div class="status-info"> 1101 + ${current.text ? `<span id="current-text">${parseLinks(current.text)}</span>` : ''} 1102 + <span class="meta">${relativeTime(current.createdAt)}${expiresHtml}</span> 1103 + </div> 1104 + </div> 1105 + </div> 1106 + `; 1107 + 1108 + if (statuses.length > 1) { 1109 + html += '<section class="history"><h2>history</h2><div class="feed-list">'; 1110 + statuses.slice(1).forEach(status => { 1111 + html += ` 1112 + <div class="status-item"> 1113 + <span class="emoji">${renderEmoji(status.emoji)}</span> 1114 + <div class="content"> 1115 + <div>${status.text ? `<span class="text">${parseLinks(status.text)}</span>` : ''}</div> 1116 + <span class="time">${relativeTime(status.createdAt)}</span> 1117 + </div> 1118 + </div> 1119 + `; 1120 + }); 1121 + html += '</div></section>'; 1122 + } 1123 + 1124 + main.innerHTML = html; 1125 + } catch (e) { 1126 + console.error('Failed to load profile:', e); 1127 + main.innerHTML = '<div class="center">failed to load profile</div>'; 1128 + } 1129 + } 1130 + 1131 + // Update nav active state - hide current page icon, show the other 1132 + function updateNavActive(page) { 1133 + const navHome = document.getElementById('nav-home'); 1134 + const navFeed = document.getElementById('nav-feed'); 1135 + // Hide the nav icon for the current page, show the other 1136 + if (navHome) navHome.classList.toggle('hidden', page === 'home'); 1137 + if (navFeed) navFeed.classList.toggle('hidden', page === 'feed'); 1138 + } 1139 + 1140 + // Initialize auth state for header (settings, logout) - used by all pages 1141 + async function initAuthUI() { 1142 + if (typeof QuicksliceClient === 'undefined') return; 1143 + 1144 + try { 1145 + client = await QuicksliceClient.createQuicksliceClient({ 1146 + server: CONFIG.server, 1147 + clientId: CONFIG.clientId, 1148 + redirectUri: window.location.origin + '/', 1149 + }); 1150 + 1151 + const isAuthed = await client.isAuthenticated(); 1152 + if (!isAuthed) return; 1153 + 1154 + const user = client.getUser(); 1155 + if (!user) return; 1156 + 1157 + // Load and apply preferences 1158 + const prefs = await loadPreferences(); 1159 + applyPreferences(prefs); 1160 + 1161 + // Show settings button and set up modal 1162 + const settingsBtn = document.getElementById('settings-btn'); 1163 + settingsBtn.classList.remove('hidden'); 1164 + const settingsModal = createSettingsModal(); 1165 + settingsBtn.addEventListener('click', () => settingsModal.open(userPreferences || prefs)); 1166 + 1167 + // Add logout button to header nav (if not already there) 1168 + if (!document.getElementById('logout-btn')) { 1169 + const nav = document.querySelector('header nav'); 1170 + const logoutBtn = document.createElement('button'); 1171 + logoutBtn.id = 'logout-btn'; 1172 + logoutBtn.className = 'nav-btn'; 1173 + logoutBtn.setAttribute('aria-label', 'log out'); 1174 + logoutBtn.setAttribute('title', 'log out'); 1175 + logoutBtn.innerHTML = ` 1176 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 1177 + <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path> 1178 + <polyline points="16 17 21 12 16 7"></polyline> 1179 + <line x1="21" y1="12" x2="9" y2="12"></line> 1180 + </svg> 1181 + `; 1182 + logoutBtn.addEventListener('click', async () => { 1183 + await client.logout(); 1184 + window.location.href = '/'; 1185 + }); 1186 + nav.appendChild(logoutBtn); 1187 + } 1188 + 1189 + return { user, prefs }; 1190 + } catch (e) { 1191 + console.error('Failed to init auth UI:', e); 1192 + return null; 1193 + } 1194 + } 1195 + 1196 + // Init 1197 + document.addEventListener('DOMContentLoaded', () => { 1198 + initTheme(); 1199 + 1200 + const themeBtn = document.getElementById('theme-toggle'); 1201 + if (themeBtn) { 1202 + themeBtn.addEventListener('click', toggleTheme); 1203 + } 1204 + 1205 + const route = getRoute(); 1206 + updateNavActive(route.page); 1207 + 1208 + if (route.page === 'home') { 1209 + renderHome(); 1210 + } else if (route.page === 'feed') { 1211 + renderFeed(); 1212 + } else if (route.page === 'profile') { 1213 + renderProfile(route.handle); 1214 + } else { 1215 + document.getElementById('main-content').innerHTML = '<div class="center">page not found</div>'; 1216 + } 1217 + });
+1614
site/bufos.json
···
··· 1 + [ 2 + "according-to-all-known-laws-of-aviation-there-is-no-way-a-bufo-should-be-able-to-fly", 3 + "add-bufo", 4 + "all-the-bufo", 5 + "angry-karen-bufo-would-like-to-speak-with-your-manager", 6 + "australian-bufo", 7 + "awesomebufo", 8 + "be-the-bufo-you-want-to-see", 9 + "bigbufo_0_0", 10 + "bigbufo_0_1", 11 + "bigbufo_0_2", 12 + "bigbufo_0_3", 13 + "bigbufo_1_0", 14 + "bigbufo_1_1", 15 + "bigbufo_1_2", 16 + "bigbufo_1_3", 17 + "bigbufo_2_0", 18 + "bigbufo_2_1", 19 + "bigbufo_2_2", 20 + "bigbufo_2_3", 21 + "bigbufo_3_0", 22 + "bigbufo_3_1", 23 + "bigbufo_3_2", 24 + "bigbufo_3_3", 25 + "blockheads-bufo", 26 + "breaking-bufo", 27 + "bronze-bufo", 28 + "buff-bufo", 29 + "bufo", 30 + "bufo_wants_his_money", 31 + "bufo-0-10", 32 + "bufo-10", 33 + "bufo-10-4", 34 + "bufo-2022", 35 + "bufo-achieving-coding-flow", 36 + "bufo-ack", 37 + "bufo-actually", 38 + "bufo-adding-bugs-to-the-code", 39 + "bufo-adidas", 40 + "bufo-ages-rapidly-in-the-void", 41 + "bufo-aight-imma-head-out", 42 + "bufo-airpods", 43 + "bufo-alarma", 44 + "bufo-all-good", 45 + "bufo-all-warm-and-fuzzy-inside", 46 + "bufo-am-i", 47 + "bufo-amaze", 48 + "bufo-ambiently-existing", 49 + "bufo-american-football", 50 + "bufo-android", 51 + "bufo-angel", 52 + "bufo-angrily-gives-you-a-birthday-gift", 53 + "bufo-angrily-gives-you-white-elephant-gift", 54 + "bufo-angry", 55 + "bufo-angry-at-fly", 56 + "bufo-angry-bullfrog-screech", 57 + "bufo-angryandfrozen", 58 + "bufo-anime-glasses", 59 + "bufo-appears", 60 + "bufo-apple", 61 + "bufo-appreciates-jwst-pillars-of-creation", 62 + "bufo-approve", 63 + "bufo-arabicus", 64 + "bufo-are-you-seeing-this", 65 + "bufo-arr", 66 + "bufo-arrr", 67 + "bufo-arrrrrr", 68 + "bufo-arrrrrrr", 69 + "bufo-arrrrrrrrr", 70 + "bufo-arrrrrrrrrrrrrrr", 71 + "bufo-artist", 72 + "bufo-asks-politely-to-stop", 73 + "bufo-assists-with-the-landing", 74 + "bufo-atc", 75 + "bufo-away", 76 + "bufo-awkward-smile", 77 + "bufo-awkward-smile-nod", 78 + "bufo-ayy", 79 + "bufo-baby", 80 + "bufo-babysits-an-urgent-ticket", 81 + "bufo-back-pat", 82 + "bufo-backpack", 83 + "bufo-backpat", 84 + "bufo-bag-of-bufos", 85 + "bufo-bait", 86 + "bufo-baker", 87 + "bufo-baller", 88 + "bufo-bandana", 89 + "bufo-banging-head-against-the-wall", 90 + "bufo-barbie", 91 + "bufo-barney", 92 + "bufo-barrister", 93 + "bufo-baseball", 94 + "bufo-basketball", 95 + "bufo-batman", 96 + "bufo-be-my-valentine", 97 + "bufo-became-a-stranger-whose-laugh-you-can-recognize-anywhere", 98 + "bufo-bee", 99 + "bufo-bee-leaf", 100 + "bufo-bee-sad", 101 + "bufo-beer", 102 + "bufo-begrudgingly-offers-you-a-plus", 103 + "bufo-begs-for-ethernet-cable", 104 + "bufo-behind-bars", 105 + "bufo-bell-pepper", 106 + "bufo-betray", 107 + "bufo-betray-but-its-a-hotdog", 108 + "bufo-big-eyes-stare", 109 + "bufo-bigfoot", 110 + "bufo-bill-pay", 111 + "bufo-bird", 112 + "bufo-birthday-but-not-particularly-happy", 113 + "bufo-black-history", 114 + "bufo-black-tea", 115 + "bufo-blank-stare", 116 + "bufo-blank-stare_0_0", 117 + "bufo-blank-stare_0_1", 118 + "bufo-blank-stare_1_0", 119 + "bufo-blank-stare_1_1", 120 + "bufo-blanket", 121 + "bufo-blem", 122 + "bufo-blep", 123 + "bufo-bless", 124 + "bufo-bless-back", 125 + "bufo-blesses-this-pr", 126 + "bufo-block", 127 + "bufo-blogging", 128 + "bufo-bloody-mary", 129 + "bufo-blows-the-magic-conch", 130 + "bufo-blue", 131 + "bufo-blueberries", 132 + "bufo-blush", 133 + "bufo-boba", 134 + "bufo-boba-army", 135 + "bufo-boi", 136 + "bufo-boiii", 137 + "bufo-bongo", 138 + "bufo-bonk", 139 + "bufo-bops-you-on-the-head-with-a-baguette", 140 + "bufo-bops-you-on-the-head-with-a-rolled-up-newspaper", 141 + "bufo-bouge", 142 + "bufo-bouncer-says-its-time-to-go-now", 143 + "bufo-bouquet", 144 + "bufo-bourgeoisie", 145 + "bufo-bowser", 146 + "bufo-box-of-chocolates", 147 + "bufo-brain", 148 + "bufo-brain-damage", 149 + "bufo-brain-damage-escalates-to-new-heights", 150 + "bufo-brain-damage-intensifies", 151 + "bufo-brain-damage-intesifies-more", 152 + "bufo-brain-exploding", 153 + "bufo-breakdown", 154 + "bufo-breaks-tech-bros-heart", 155 + "bufo-breaks-up-with-you", 156 + "bufo-breaks-your-heart", 157 + "bufo-brick", 158 + "bufo-brings-a-new-meaning-to-brain-freeze-by-bopping-you-on-the-head-with-a-popsicle", 159 + "bufo-brings-a-new-meaning-to-gaveled-by-slamming-the-hammer-very-loud", 160 + "bufo-brings-magic-to-the-riot", 161 + "bufo-broccoli", 162 + "bufo-broke", 163 + "bufo-broke-his-toe-and-isn't-sure-what-to-do-about-the-12k-he-signed-up-for", 164 + "bufo-broom", 165 + "bufo-brought-a-taco", 166 + "bufo-bufo", 167 + "bufo-but-anatomically-correct", 168 + "bufo-but-instead-of-green-its-hotdogs", 169 + "bufo-but-instead-of-green-its-pizza", 170 + "bufo-but-you-can-feel-the-electro-house-music-in-the-gif-and-oh-yea-theres-also-a-dapper-chicken", 171 + "bufo-but-you-can-see-the-bufo-in-bufos-eyes", 172 + "bufo-but-you-can-see-the-hotdog-in-their-eyes", 173 + "bufo-buy-high-sell-low", 174 + "bufo-buy-low-sell-high", 175 + "bufo-cache-buddy", 176 + "bufo-cackle", 177 + "bufo-call-for-help", 178 + "bufo-came-into-the-office-just-to-use-the-printer", 179 + "bufo-can't-believe-heartbreak-feels-good-in-a-place-like-this", 180 + "bufo-can't-help-but-wonder-who-watches-the-watchmen", 181 + "bufo-canada", 182 + "bufo-cant-believe-your-audacity", 183 + "bufo-cant-find-a-pull-request", 184 + "bufo-cant-find-an-issue", 185 + "bufo-cant-stop-thinking-about-usher-killing-it-on-roller-skates", 186 + "bufo-cant-take-it-anymore", 187 + "bufo-cantelope", 188 + "bufo-capri-sun", 189 + "bufo-captain-obvious", 190 + "bufo-caribou", 191 + "bufo-carnage", 192 + "bufo-carrot", 193 + "bufo-cash-money", 194 + "bufo-cash-squint", 195 + "bufo-casts-a-spell-on-you", 196 + "bufo-catch", 197 + "bufo-caught-a-radioactive-bufo", 198 + "bufo-caught-a-small-bufo", 199 + "bufo-caused-an-incident", 200 + "bufo-celebrate", 201 + "bufo-censored", 202 + "bufo-chappell-roan", 203 + "bufo-chatting", 204 + "bufo-check", 205 + "bufo-checks-out-the-vibe", 206 + "bufo-cheese", 207 + "bufo-chef", 208 + "bufo-chefkiss", 209 + "bufo-chefkiss-with-hat", 210 + "bufo-cherries", 211 + "bufo-chicken", 212 + "bufo-chomp", 213 + "bufo-christmas", 214 + "bufo-chungus", 215 + "bufo-churns-the-butter", 216 + "bufo-clap", 217 + "bufo-clap-hd", 218 + "bufo-claus", 219 + "bufo-clown", 220 + "bufo-coconut", 221 + "bufo-code-freeze", 222 + "bufo-coding", 223 + "bufo-coffee-happy", 224 + "bufo-coin", 225 + "bufo-come-to-the-dark-side", 226 + "bufo-comfy", 227 + "bufo-commits-digital-piracy", 228 + "bufo-competes-in-the-bufo-bracket", 229 + "bufo-complies-with-the-chinese-government", 230 + "bufo-concerned", 231 + "bufo-cone-of-shame", 232 + "bufo-confetti", 233 + "bufo-confused", 234 + "bufo-congrats", 235 + "bufo-cookie", 236 + "bufo-cool-glasses", 237 + "bufo-corn", 238 + "bufo-cornucopia", 239 + "bufo-covid", 240 + "bufo-cowboy", 241 + "bufo-cozy-blanky", 242 + "bufo-crewmate-blue", 243 + "bufo-crewmate-blue-bounce", 244 + "bufo-crewmate-cyan", 245 + "bufo-crewmate-cyan-bounce", 246 + "bufo-crewmate-green", 247 + "bufo-crewmate-green-bounce", 248 + "bufo-crewmate-lime", 249 + "bufo-crewmate-lime-bounce", 250 + "bufo-crewmate-orange", 251 + "bufo-crewmate-orange-bounce", 252 + "bufo-crewmate-pink", 253 + "bufo-crewmate-pink-bounce", 254 + "bufo-crewmate-purple", 255 + "bufo-crewmate-purple-bounce", 256 + "bufo-crewmate-red", 257 + "bufo-crewmate-red-bounce", 258 + "bufo-crewmate-yellow", 259 + "bufo-crewmate-yellow-bounce", 260 + "bufo-crewmates", 261 + "bufo-cries-into-his-beer", 262 + "bufo-crikey", 263 + "bufo-croptop", 264 + "bufo-crumbs", 265 + "bufo-crustacean", 266 + "bufo-cry", 267 + "bufo-cry-pray", 268 + "bufo-crying", 269 + "bufo-crying-in-the-rain", 270 + "bufo-crying-jail", 271 + "bufo-crying-stop", 272 + "bufo-crying-tears-of-crying-tears-of-joy", 273 + "bufo-crying-why", 274 + "bufo-cubo", 275 + "bufo-cucumber", 276 + "bufo-cuddle", 277 + "bufo-cupcake", 278 + "bufo-cuppa", 279 + "bufo-cute", 280 + "bufo-cute-dance", 281 + "bufo-dab", 282 + "bufo-dancing", 283 + "bufo-dapper", 284 + "bufo-dbz", 285 + "bufo-deal-with-it", 286 + "bufo-declines-your-suppository-offer", 287 + "bufo-deep-hmm", 288 + "bufo-defend", 289 + "bufo-delurk", 290 + "bufo-demands-more-nom-noms", 291 + "bufo-demure", 292 + "bufo-desperately-needs-mavis-beacon", 293 + "bufo-detective", 294 + "bufo-develops-clairvoyance-while-trapped-in-the-void", 295 + "bufo-devil", 296 + "bufo-devouring-his-son", 297 + "bufo-di-beppo", 298 + "bufo-did-not-make-it-through-the-heatwave", 299 + "bufo-didnt-get-any-sleep", 300 + "bufo-didnt-listen-to-willy-wonka", 301 + "bufo-disappointed", 302 + "bufo-disco", 303 + "bufo-discombobulated", 304 + "bufo-disguise", 305 + "bufo-ditto", 306 + "bufo-dizzy", 307 + "bufo-do-not-panic", 308 + "bufo-dodge", 309 + "bufo-doesnt-believe-you", 310 + "bufo-doesnt-understand-how-this-meeting-isnt-an-email", 311 + "bufo-doesnt-wanna-get-out-of-the-bath-yet", 312 + "bufo-dog", 313 + "bufo-domo", 314 + "bufo-done-check", 315 + "bufo-dont", 316 + "bufo-dont-even-see-the-code-anymore", 317 + "bufo-dont-trust-whats-over-there", 318 + "bufo-double-chin", 319 + "bufo-double-vaccinated", 320 + "bufo-doubt", 321 + "bufo-dough", 322 + "bufo-downvote", 323 + "bufo-dr-depper", 324 + "bufo-dragon", 325 + "bufo-drags-knee", 326 + "bufo-drake-no", 327 + "bufo-drake-yes", 328 + "bufo-drifts-through-the-void", 329 + "bufo-drinking-baja-blast", 330 + "bufo-drinking-boba", 331 + "bufo-drinking-coffee", 332 + "bufo-drinking-coke", 333 + "bufo-drinking-pepsi", 334 + "bufo-drinking-pumpkin-spice-latte", 335 + "bufo-drinks-from-the-fire-hose", 336 + "bufo-drops-everything-now", 337 + "bufo-drowning-in-leeks", 338 + "bufo-drowns-in-memories-of-ocean", 339 + "bufo-drowns-in-tickets-but-ok", 340 + "bufo-drumroll", 341 + "bufo-easter-bunny", 342 + "bufo-eating-hotdog", 343 + "bufo-eating-lollipop", 344 + "bufo-eats-a-bufo-taco", 345 + "bufo-eats-all-your-honey", 346 + "bufo-eats-bufo-taco", 347 + "bufo-egg", 348 + "bufo-elite", 349 + "bufo-emo", 350 + "bufo-ends-the-holy-war-by-offering-the-objectively-best-programming-language", 351 + "bufo-enjoys-life", 352 + "bufo-enjoys-life-in-the-windows-xp-background", 353 + "bufo-enraged", 354 + "bufo-enter", 355 + "bufo-enters-the-void", 356 + "bufo-entrance", 357 + "bufo-ethereum", 358 + "bufo-everything-is-on-fire", 359 + "bufo-evil", 360 + "bufo-excited", 361 + "bufo-excited-but-sad", 362 + "bufo-existential-dread-sets-in", 363 + "bufo-exit", 364 + "bufo-experiences-euneirophrenia", 365 + "bufo-extra-cool", 366 + "bufo-eye-twitch", 367 + "bufo-eyeballs", 368 + "bufo-eyeballs-bloodshot", 369 + "bufo-eyes", 370 + "bufo-fab", 371 + "bufo-facepalm", 372 + "bufo-failed-the-load-test", 373 + "bufo-fails-the-vibe-check", 374 + "bufo-fancy-tea", 375 + "bufo-farmer", 376 + "bufo-fastest-rubber-stamp-in-the-west", 377 + "bufo-fedora", 378 + "bufo-feel-better", 379 + "bufo-feeling-pretty-might-delete-later", 380 + "bufo-feels-appreciated", 381 + "bufo-feels-nothing", 382 + "bufo-fell-asleep", 383 + "bufo-fellow-kids", 384 + "bufo-fieri", 385 + "bufo-fight", 386 + "bufo-fine-art", 387 + "bufo-fingerguns", 388 + "bufo-fingerguns-back", 389 + "bufo-fire", 390 + "bufo-fire-engine", 391 + "bufo-firefighter", 392 + "bufo-fish", 393 + "bufo-fish-bulb", 394 + "bufo-fistbump", 395 + "bufo-flex", 396 + "bufo-flipoff", 397 + "bufo-flips-table", 398 + "bufo-folder", 399 + "bufo-fomo", 400 + "bufo-food-please", 401 + "bufo-football", 402 + "bufo-for-dummies", 403 + "bufo-forgot-how-to-type", 404 + "bufo-forgot-that-you-existed-it-isnt-love-it-isnt-hate-its-just-indifference", 405 + "bufo-found-some-more-leeks", 406 + "bufo-found-the-leeks", 407 + "bufo-found-yet-another-juicebox", 408 + "bufo-french", 409 + "bufo-friends", 410 + "bufo-frustrated-with-flower", 411 + "bufo-fu%C3%9Fball", 412 + "bufo-fun-is-over", 413 + "bufo-furiously-tries-to-write-python", 414 + "bufo-furiously-writes-an-epic-update", 415 + "bufo-furiously-writes-you-a-peer-review", 416 + "bufo-futbol", 417 + "bufo-gamer", 418 + "bufo-gaming", 419 + "bufo-gandalf", 420 + "bufo-gandalf-has-seen-things", 421 + "bufo-gandalf-wat", 422 + "bufo-gardener", 423 + "bufo-garlic", 424 + "bufo-gavel", 425 + "bufo-gavel-dual-wield", 426 + "bufo-gen-z", 427 + "bufo-gentleman", 428 + "bufo-germany", 429 + "bufo-get-in-loser-were-going-shopping", 430 + "bufo-gets-downloaded-from-the-cloud", 431 + "bufo-gets-hit-in-the-face-with-an-egg", 432 + "bufo-gets-uploaded-to-the-cloud", 433 + "bufo-gets-whiplash", 434 + "bufo-ghost", 435 + "bufo-ghost-costume", 436 + "bufo-giggling-in-a-cat-onesie", 437 + "bufo-give", 438 + "bufo-give-money", 439 + "bufo-give-pack-of-ice", 440 + "bufo-gives-a-fake-moustache", 441 + "bufo-gives-a-magic-number", 442 + "bufo-gives-an-idea", 443 + "bufo-gives-approval", 444 + "bufo-gives-can-of-worms", 445 + "bufo-gives-databricks", 446 + "bufo-gives-j", 447 + "bufo-gives-star", 448 + "bufo-gives-you-a-feature-flag", 449 + "bufo-gives-you-a-hotdog", 450 + "bufo-gives-you-some-extra-brain", 451 + "bufo-gives-you-some-rice", 452 + "bufo-glasses", 453 + "bufo-glitch", 454 + "bufo-goal", 455 + "bufo-goes-super-saiyan", 456 + "bufo-goes-to-space", 457 + "bufo-goggles-are-too-tight", 458 + "bufo-good-morning", 459 + "bufo-good-vibe", 460 + "bufo-goose-hat-happy-dance", 461 + "bufo-got-a-tan", 462 + "bufo-got-zapped", 463 + "bufo-grapes", 464 + "bufo-grasping-at-straws", 465 + "bufo-grenade", 466 + "bufo-grimaces-with-eyebrows", 467 + "bufo-guitar", 468 + "bufo-ha-ha", 469 + "bufo-hacker", 470 + "bufo-hackerman", 471 + "bufo-haha-yes-haha-yes", 472 + "bufo-hahabusiness", 473 + "bufo-halloween", 474 + "bufo-halloween-pumpkin", 475 + "bufo-hands", 476 + "bufo-hands-on-hips-annoyed", 477 + "bufo-hangs-ten", 478 + "bufo-hangs-up", 479 + "bufo-hannibal-lecter", 480 + "bufo-hanson", 481 + "bufo-happy", 482 + "bufo-happy-hour", 483 + "bufo-happy-new-year", 484 + "bufo-hardhat", 485 + "bufo-has-a-5-dollar-footlong", 486 + "bufo-has-a-banana", 487 + "bufo-has-a-bbq", 488 + "bufo-has-a-big-wrench", 489 + "bufo-has-a-blue-wrench", 490 + "bufo-has-a-crush", 491 + "bufo-has-a-dr-pepper", 492 + "bufo-has-a-fresh-slice", 493 + "bufo-has-a-headache", 494 + "bufo-has-a-hot-take", 495 + "bufo-has-a-question", 496 + "bufo-has-a-sandwich", 497 + "bufo-has-a-spoon", 498 + "bufo-has-a-timtam", 499 + "bufo-has-accepted-its-horrible-fate", 500 + "bufo-has-activated", 501 + "bufo-has-another-sandwich", 502 + "bufo-has-been-cleaning", 503 + "bufo-has-gotta-poop-but-hes-stuck-in-a-long-meeting", 504 + "bufo-has-infiltrated-your-secure-system", 505 + "bufo-has-midas-touch", 506 + "bufo-has-read-enough-documentation-for-today", 507 + "bufo-has-some-ketchup", 508 + "bufo-has-thread-for-guts", 509 + "bufo-hasnt-worked-a-full-week-so-far-this-year", 510 + "bufo-hat", 511 + "bufo-hazmat", 512 + "bufo-headbang", 513 + "bufo-headphones", 514 + "bufo-heart", 515 + "bufo-heart-but-its-anatomically-correct", 516 + "bufo-hearts", 517 + "bufo-hehe", 518 + "bufo-hell", 519 + "bufo-hello", 520 + "bufo-heralds-an-incident", 521 + "bufo-heralds-taco-taking", 522 + "bufo-heralds-your-success", 523 + "bufo-here-to-make-a-dill-for-more-pickles", 524 + "bufo-hides", 525 + "bufo-high-speed-train", 526 + "bufo-highfive-1", 527 + "bufo-highfive-2", 528 + "bufo-hipster", 529 + "bufo-hmm", 530 + "bufo-hmm-no", 531 + "bufo-hmm-yes", 532 + "bufo-holding-space-for-defying-gravity", 533 + "bufo-holds-pumpkin", 534 + "bufo-homologates", 535 + "bufo-hop-in-we're-going-to-flavortown", 536 + "bufo-hopes-you-also-are-having-a-good-day", 537 + "bufo-hopes-you-are-having-a-good-day", 538 + "bufo-hot-pocket", 539 + "bufo-hotdog-rocket", 540 + "bufo-howdy", 541 + "bufo-hug", 542 + "bufo-hugs-moo-deng", 543 + "bufo-hype", 544 + "bufo-i-just-love-it-so-much", 545 + "bufo-ice-cream", 546 + "bufo-idk", 547 + "bufo-idk-but-okay-i-guess-so", 548 + "bufo-im-in-danger", 549 + "bufo-imposter", 550 + "bufo-in-a-pear-tree", 551 + "bufo-in-his-cozy-bed-hoping-he-never-gets-capitated", 552 + "bufo-in-rome", 553 + "bufo-inception", 554 + "bufo-increases-his-dimensionality-while-trapped-in-the-void", 555 + "bufo-innocent", 556 + "bufo-inspecting", 557 + "bufo-inspired", 558 + "bufo-instigates-a-dramatic-turn-of-events", 559 + "bufo-intensifies", 560 + "bufo-intern", 561 + "bufo-investigates", 562 + "bufo-iphone", 563 + "bufo-irl", 564 + "bufo-iron-throne", 565 + "bufo-ironside", 566 + "bufo-is-a-little-worried-but-still-trying-to-be-supportive", 567 + "bufo-is-a-part-of-gen-z", 568 + "bufo-is-about-to-zap-you", 569 + "bufo-is-all-ears", 570 + "bufo-is-angry-at-the-water-cooler-bottle-company-for-missing-yet-another-delivery", 571 + "bufo-is-at-his-wits-end", 572 + "bufo-is-at-the-dentist", 573 + "bufo-is-better-known-for-the-things-he-does-on-the-mattress", 574 + "bufo-is-exhausted-rooting-for-the-antihero", 575 + "bufo-is-flying-and-is-the-plane", 576 + "bufo-is-getting-abducted", 577 + "bufo-is-getting-paged-now", 578 + "bufo-is-glad-the-british-were-kicked-out", 579 + "bufo-is-happy-youre-happy", 580 + "bufo-is-having-a-really-bad-time", 581 + "bufo-is-in-a-never-ending-meeting", 582 + "bufo-is-in-on-the-joke", 583 + "bufo-is-inhaling-this-popcorn", 584 + "bufo-is-it-done", 585 + "bufo-is-jealous-its-your-birthday", 586 + "bufo-is-jean-baptise-emanuel-zorg", 587 + "bufo-is-keeping-his-eye-on-you", 588 + "bufo-is-lonely", 589 + "bufo-is-lost", 590 + "bufo-is-lost-in-the-void", 591 + "bufo-is-omniscient", 592 + "bufo-is-on-a-sled", 593 + "bufo-is-panicking", 594 + "bufo-is-petting-your-cat", 595 + "bufo-is-petting-your-dog", 596 + "bufo-is-proud-of-you", 597 + "bufo-is-ready-for-xmas", 598 + "bufo-is-ready-to-build-when-you-are", 599 + "bufo-is-ready-to-burn-down-the-mta-because-their-train-skipped-their-station-again", 600 + "bufo-is-ready-to-consume-his-daily-sodium-intake-in-one-sitting", 601 + "bufo-is-ready-to-eat", 602 + "bufo-is-ready-to-riot", 603 + "bufo-is-ready-to-slay-the-dragon", 604 + "bufo-is-romantic", 605 + "bufo-is-sad-no-one-complimented-their-agent-47-cosplay", 606 + "bufo-is-safe-behind-bars", 607 + "bufo-is-so-happy-youre-here", 608 + "bufo-is-the-perfect-human-form", 609 + "bufo-is-trapped-in-a-cameron-winter-phase", 610 + "bufo-is-unconcerned", 611 + "bufo-is-up-to-something", 612 + "bufo-is-very-upset-now", 613 + "bufo-is-watching-you", 614 + "bufo-is-working-through-the-tears", 615 + "bufo-is-working-too-much", 616 + "bufo-isitdone", 617 + "bufo-isnt-angry-just-disappointed", 618 + "bufo-isnt-going-to-rewind-the-vhs-before-returning-it", 619 + "bufo-isnt-reading-all-that", 620 + "bufo-it-bar", 621 + "bufo-italian", 622 + "bufo-its-over-9000", 623 + "bufo-its-too-early-for-this", 624 + "bufo-jam", 625 + "bufo-jammies", 626 + "bufo-jammin", 627 + "bufo-jealous", 628 + "bufo-jedi", 629 + "bufo-jomo", 630 + "bufo-judge", 631 + "bufo-judges", 632 + "bufo-juice", 633 + "bufo-juicebox", 634 + "bufo-juicy", 635 + "bufo-just-a-little-sad", 636 + "bufo-just-a-little-salty", 637 + "bufo-just-checking", 638 + "bufo-just-finished-a-workout", 639 + "bufo-just-got-back-from-the-dentist", 640 + "bufo-just-ice", 641 + "bufo-just-walked-into-an-awkward-conversation-and-is-now-trying-to-figure-out-how-to-leave", 642 + "bufo-just-wanted-you-to-know-this-is-him-trying", 643 + "bufo-justice", 644 + "bufo-karen", 645 + "bufo-keeps-his-password-written-on-a-post-it-note-stuck-to-his-monitor", 646 + "bufo-keyboard", 647 + "bufo-kills-you-with-kindness", 648 + "bufo-king", 649 + "bufo-kiwi", 650 + "bufo-knife", 651 + "bufo-knife-cries-right", 652 + "bufo-knife-crying", 653 + "bufo-knife-crying-left", 654 + "bufo-knife-crying-right", 655 + "bufo-knows-age-is-just-a-number", 656 + "bufo-knows-his-customers", 657 + "bufo-knows-this-is-a-total-bop", 658 + "bufo-knuckle-sandwich", 659 + "bufo-knuckles", 660 + "bufo-koi", 661 + "bufo-kudo", 662 + "bufo-kuzco", 663 + "bufo-kuzco-has-not-learned-his-lesson-yet", 664 + "bufo-laser-eyes", 665 + "bufo-late-to-the-convo", 666 + "bufo-laugh-xd", 667 + "bufo-laughing-popcorn", 668 + "bufo-laughs-to-mask-the-pain", 669 + "bufo-leads-the-way-to-better-docs", 670 + "bufo-leaves-you-on-seen", 671 + "bufo-left-a-comment", 672 + "bufo-left-multiple-comments", 673 + "bufo-legal-entities", 674 + "bufo-lemon", 675 + "bufo-leprechaun", 676 + "bufo-let-them-eat-cake", 677 + "bufo-lgtm", 678 + "bufo-liberty", 679 + "bufo-liberty-forgot-her-torch", 680 + "bufo-librarian", 681 + "bufo-lick", 682 + "bufo-licks-his-hway-out-of-prison", 683 + "bufo-lies-awake-in-panic", 684 + "bufo-life-saver", 685 + "bufo-likes-that-idea", 686 + "bufo-link", 687 + "bufo-listens-to-his-conscience", 688 + "bufo-lit", 689 + "bufo-littlefoot-is-upset", 690 + "bufo-loading", 691 + "bufo-lol", 692 + "bufo-lol-cry", 693 + "bufo-lolsob", 694 + "bufo-long", 695 + "bufo-lookin-dope", 696 + "bufo-looking-very-much", 697 + "bufo-looks-a-little-closer", 698 + "bufo-looks-for-a-pull-request", 699 + "bufo-looks-for-an-issue", 700 + "bufo-looks-like-hes-listening-but-hes-not", 701 + "bufo-looks-out-of-the-window", 702 + "bufo-loves-blobs", 703 + "bufo-loves-disco", 704 + "bufo-loves-doges", 705 + "bufo-loves-pho", 706 + "bufo-loves-rice-and-beans", 707 + "bufo-loves-ruby", 708 + "bufo-loves-this-song", 709 + "bufo-luigi", 710 + "bufo-lunch", 711 + "bufo-lurk", 712 + "bufo-lurk-delurk", 713 + "bufo-macbook", 714 + "bufo-made-salad", 715 + "bufo-made-you-a-burrito", 716 + "bufo-magician", 717 + "bufo-make-it-rain", 718 + "bufo-makes-it-rain", 719 + "bufo-makes-the-dream-work", 720 + "bufo-mama-mia-thatsa-one-spicy-a-meatball", 721 + "bufo-marine", 722 + "bufo-mario", 723 + "bufo-mask", 724 + "bufo-matrix", 725 + "bufo-medal", 726 + "bufo-meltdown", 727 + "bufo-melting", 728 + "bufo-micdrop", 729 + "bufo-midsommar", 730 + "bufo-midwest-princess", 731 + "bufo-mild-panic", 732 + "bufo-mildly-aggravated", 733 + "bufo-milk", 734 + "bufo-mindblown", 735 + "bufo-minecraft-attack", 736 + "bufo-minecraft-defend", 737 + "bufo-mischievous", 738 + "bufo-mitosis", 739 + "bufo-mittens", 740 + "bufo-modern-art", 741 + "bufo-monocle", 742 + "bufo-monstera", 743 + "bufo-morning", 744 + "bufo-morning-starbucks", 745 + "bufo-morning-sun", 746 + "bufo-mrtayto", 747 + "bufo-mushroom", 748 + "bufo-mustache", 749 + "bufo-my-pho", 750 + "bufo-nah", 751 + "bufo-naked", 752 + "bufo-naptime", 753 + "bufo-needs-some-hot-tea-to-process-this-news", 754 + "bufo-needs-to-vent", 755 + "bufo-nefarious", 756 + "bufo-nervous", 757 + "bufo-nervous-but-cute", 758 + "bufo-night", 759 + "bufo-ninja", 760 + "bufo-no", 761 + "bufo-no-capes", 762 + "bufo-no-more-today-thank-you", 763 + "bufo-no-prob", 764 + "bufo-no-problem", 765 + "bufo-no-ragrets", 766 + "bufo-no-sleep", 767 + "bufo-no-u", 768 + "bufo-nod", 769 + "bufo-noodles", 770 + "bufo-nope", 771 + "bufo-nosy", 772 + "bufo-not-bad-by-dalle", 773 + "bufo-not-my-problem", 774 + "bufo-not-respecting-your-personal-space", 775 + "bufo-notice-me-senpai", 776 + "bufo-notification", 777 + "bufo-np", 778 + "bufo-nun", 779 + "bufo-nyc", 780 + "bufo-oatly", 781 + "bufo-oblivious-and-innocent", 782 + "bufo-of-liberty", 783 + "bufo-offering-bufo-offering-bufo-offering-bufo", 784 + "bufo-offers-1", 785 + "bufo-offers-13", 786 + "bufo-offers-2", 787 + "bufo-offers-200", 788 + "bufo-offers-21", 789 + "bufo-offers-3", 790 + "bufo-offers-5", 791 + "bufo-offers-8", 792 + "bufo-offers-a-bagel", 793 + "bufo-offers-a-ball-of-mud", 794 + "bufo-offers-a-banana-in-these-trying-times", 795 + "bufo-offers-a-beer", 796 + "bufo-offers-a-bicycle", 797 + "bufo-offers-a-bolillo-para-el-susto", 798 + "bufo-offers-a-book", 799 + "bufo-offers-a-brain", 800 + "bufo-offers-a-bufo-egg-in-this-trying-time", 801 + "bufo-offers-a-burger", 802 + "bufo-offers-a-cake", 803 + "bufo-offers-a-clover", 804 + "bufo-offers-a-comment", 805 + "bufo-offers-a-cookie", 806 + "bufo-offers-a-deploy-lock", 807 + "bufo-offers-a-factory", 808 + "bufo-offers-a-flan", 809 + "bufo-offers-a-flowchart-to-help-you-navigate-this-workflow", 810 + "bufo-offers-a-focaccia", 811 + "bufo-offers-a-furby", 812 + "bufo-offers-a-gavel", 813 + "bufo-offers-a-generator", 814 + "bufo-offers-a-hario-scale", 815 + "bufo-offers-a-hot-take", 816 + "bufo-offers-a-jetpack-zebra", 817 + "bufo-offers-a-kakapo", 818 + "bufo-offers-a-like", 819 + "bufo-offers-a-little-band-aid-for-a-big-problem", 820 + "bufo-offers-a-llama", 821 + "bufo-offers-a-loading-spinner", 822 + "bufo-offers-a-loading-spinner-spinning", 823 + "bufo-offers-a-lock", 824 + "bufo-offers-a-mac-m1-chip", 825 + "bufo-offers-a-pager", 826 + "bufo-offers-a-piece-of-cake", 827 + "bufo-offers-a-pr", 828 + "bufo-offers-a-pull-request", 829 + "bufo-offers-a-rock", 830 + "bufo-offers-a-roomba", 831 + "bufo-offers-a-ruby", 832 + "bufo-offers-a-sandbox", 833 + "bufo-offers-a-shocked-pikachu", 834 + "bufo-offers-a-speedy-recovery", 835 + "bufo-offers-a-status", 836 + "bufo-offers-a-taco", 837 + "bufo-offers-a-telescope", 838 + "bufo-offers-a-tiny-wood-stove", 839 + "bufo-offers-a-torta-ahogada", 840 + "bufo-offers-a-webhook", 841 + "bufo-offers-a-webhook-but-the-logo-is-canonically-correct", 842 + "bufo-offers-a-wednesday", 843 + "bufo-offers-a11y", 844 + "bufo-offers-ai", 845 + "bufo-offers-airwrap", 846 + "bufo-offers-an-airpod-pro", 847 + "bufo-offers-an-easter-egg", 848 + "bufo-offers-an-eclair", 849 + "bufo-offers-an-egg-in-this-trying-time", 850 + "bufo-offers-an-ethernet-cable", 851 + "bufo-offers-an-export-of-your-data", 852 + "bufo-offers-an-extinguisher", 853 + "bufo-offers-an-idea", 854 + "bufo-offers-an-incident", 855 + "bufo-offers-an-issue", 856 + "bufo-offers-an-outage", 857 + "bufo-offers-approval", 858 + "bufo-offers-avocado", 859 + "bufo-offers-bento", 860 + "bufo-offers-big-band-aid-for-a-little-problem", 861 + "bufo-offers-bitcoin", 862 + "bufo-offers-boba", 863 + "bufo-offers-boss-coffee", 864 + "bufo-offers-box", 865 + "bufo-offers-bufo", 866 + "bufo-offers-bufo-cubo", 867 + "bufo-offers-bufo-offers", 868 + "bufo-offers-bufomelon", 869 + "bufo-offers-calculated-decision-to-leave-tech-debt-for-now-and-clean-it-up-later", 870 + "bufo-offers-caribufo", 871 + "bufo-offers-chart-with-upwards-trend", 872 + "bufo-offers-chatgpt", 873 + "bufo-offers-chrome", 874 + "bufo-offers-coffee", 875 + "bufo-offers-copilot", 876 + "bufo-offers-corn", 877 + "bufo-offers-corporate-red-tape", 878 + "bufo-offers-covid", 879 + "bufo-offers-csharp", 880 + "bufo-offers-d20", 881 + "bufo-offers-datadog", 882 + "bufo-offers-discord", 883 + "bufo-offers-dnd", 884 + "bufo-offers-empty-wallet", 885 + "bufo-offers-f5", 886 + "bufo-offers-factorio", 887 + "bufo-offers-falafel", 888 + "bufo-offers-fart-cloud", 889 + "bufo-offers-firefox", 890 + "bufo-offers-flatbread", 891 + "bufo-offers-footsie", 892 + "bufo-offers-friday", 893 + "bufo-offers-fud", 894 + "bufo-offers-gatorade", 895 + "bufo-offers-git-mailing-list", 896 + "bufo-offers-golden-handcuffs", 897 + "bufo-offers-google-doc", 898 + "bufo-offers-google-drive", 899 + "bufo-offers-google-sheets", 900 + "bufo-offers-hello-kitty", 901 + "bufo-offers-help", 902 + "bufo-offers-hotdog", 903 + "bufo-offers-jira", 904 + "bufo-offers-ldap", 905 + "bufo-offers-lego", 906 + "bufo-offers-model-1857-12-pounder-napoleon-cannon", 907 + "bufo-offers-moneybag", 908 + "bufo-offers-new-jira", 909 + "bufo-offers-nothing", 910 + "bufo-offers-notion", 911 + "bufo-offers-oatmilk", 912 + "bufo-offers-openai", 913 + "bufo-offers-pancakes", 914 + "bufo-offers-peanuts", 915 + "bufo-offers-pineapple", 916 + "bufo-offers-power", 917 + "bufo-offers-prescription-strength-painkillers", 918 + "bufo-offers-python", 919 + "bufo-offers-securifriend", 920 + "bufo-offers-solar-eclipse", 921 + "bufo-offers-spam", 922 + "bufo-offers-stash-of-tea-from-the-office-for-the-weekend", 923 + "bufo-offers-tayto", 924 + "bufo-offers-terraform", 925 + "bufo-offers-the-cloud", 926 + "bufo-offers-the-power", 927 + "bufo-offers-the-weeknd", 928 + "bufo-offers-thoughts-and-prayers", 929 + "bufo-offers-thread", 930 + "bufo-offers-thundercats", 931 + "bufo-offers-tim-tams", 932 + "bufo-offers-tree", 933 + "bufo-offers-turkish-delights", 934 + "bufo-offers-ube", 935 + "bufo-offers-watermelon", 936 + "bufo-offers-you-a-comically-oversized-waffle", 937 + "bufo-offers-you-a-db-for-your-customer-data", 938 + "bufo-offers-you-a-gdpr-compliant-cookie", 939 + "bufo-offers-you-a-kfc-16-piece-family-size-bucket-of-fried-chicken", 940 + "bufo-offers-you-a-monster-early-in-the-morning", 941 + "bufo-offers-you-a-pint-m8", 942 + "bufo-offers-you-a-red-bull-early-in-the-morning", 943 + "bufo-offers-you-a-suspiciously-not-urgent-ticket", 944 + "bufo-offers-you-an-urgent-ticket", 945 + "bufo-offers-you-dangerously-high-rate-limits", 946 + "bufo-offers-you-his-crypto-before-he-pumps-and-dumps-it", 947 + "bufo-offers-you-logs", 948 + "bufo-offers-you-money-in-this-trying-time", 949 + "bufo-offers-you-the-best-emoji-culture-ever", 950 + "bufo-offers-you-the-moon", 951 + "bufo-offers-you-the-world", 952 + "bufo-offers-yubikey", 953 + "bufo-office", 954 + "bufo-oh-hai", 955 + "bufo-oh-no", 956 + "bufo-oh-yeah", 957 + "bufo-ok", 958 + "bufo-okay-pretty-salty-now", 959 + "bufo-old", 960 + "bufo-olives", 961 + "bufo-omg", 962 + "bufo-on-fire-but-still-excited", 963 + "bufo-on-the-ceiling", 964 + "bufo-oncall-secondary", 965 + "bufo-onion", 966 + "bufo-open-mic", 967 + "bufo-opens-a-haberdashery", 968 + "bufo-orange", 969 + "bufo-oreilly", 970 + "bufo-pager-duty", 971 + "bufo-pajama-party", 972 + "bufo-palpatine", 973 + "bufo-panic", 974 + "bufo-parrot", 975 + "bufo-party", 976 + "bufo-party-birthday", 977 + "bufo-party-conga-line", 978 + "bufo-passed-the-load-test", 979 + "bufo-passes-the-vibe-check", 980 + "bufo-pat", 981 + "bufo-peaks-on-you-from-above", 982 + "bufo-peaky-blinder", 983 + "bufo-pear", 984 + "bufo-pearly-whites", 985 + "bufo-peek", 986 + "bufo-peek-wall", 987 + "bufo-peeking", 988 + "bufo-pensivity-turned-discomfort-upon-realization-of-reality", 989 + "bufo-phew", 990 + "bufo-phonecall", 991 + "bufo-photographer", 992 + "bufo-picked-you-a-flower", 993 + "bufo-pikmin", 994 + "bufo-pilgrim", 995 + "bufo-pilot", 996 + "bufo-pinch-hitter", 997 + "bufo-pineapple", 998 + "bufo-ping", 999 + "bufo-pirate", 1000 + "bufo-pitchfork", 1001 + "bufo-pitchforks", 1002 + "bufo-pizza-hut", 1003 + "bufo-placeholder", 1004 + "bufo-platformizes", 1005 + "bufo-plays-some-smooth-jazz", 1006 + "bufo-plays-some-smooth-jazz-intensity-1", 1007 + "bufo-pleading", 1008 + "bufo-pleading-1", 1009 + "bufo-please", 1010 + "bufo-pog", 1011 + "bufo-pog-surprise", 1012 + "bufo-pointing-down-there", 1013 + "bufo-pointing-over-there", 1014 + "bufo-pointing-right-there", 1015 + "bufo-pointing-up-there", 1016 + "bufo-police", 1017 + "bufo-poliwhirl", 1018 + "bufo-ponders", 1019 + "bufo-ponders-2", 1020 + "bufo-ponders-3", 1021 + "bufo-poo", 1022 + "bufo-poof", 1023 + "bufo-popcorn", 1024 + "bufo-popping-out-of-the-coffee", 1025 + "bufo-popping-out-of-the-coffee-upsidedown", 1026 + "bufo-popping-out-of-the-toilet", 1027 + "bufo-pops-by", 1028 + "bufo-pops-out-for-a-quick-bite-to-eat", 1029 + "bufo-possessed", 1030 + "bufo-potato", 1031 + "bufo-pours-one-out", 1032 + "bufo-praise", 1033 + "bufo-pray", 1034 + "bufo-pray-partying", 1035 + "bufo-praying-his-qa-is-on-point", 1036 + "bufo-prays-for-this-to-be-over-already", 1037 + "bufo-prays-for-this-to-be-over-already-intensifies", 1038 + "bufo-prays-to-azure", 1039 + "bufo-prays-to-nvidia", 1040 + "bufo-prays-to-pagerduty", 1041 + "bufo-preach", 1042 + "bufo-presents-to-the-bufos", 1043 + "bufo-pretends-to-have-authority", 1044 + "bufo-pretty-dang-sad", 1045 + "bufo-pride", 1046 + "bufo-psychic", 1047 + "bufo-pumpkin", 1048 + "bufo-pumpkin-head", 1049 + "bufo-pushes-to-prod", 1050 + "bufo-put-on-active-noise-cancelling-headphones-but-can-still-hear-you", 1051 + "bufo-quadruple-vaccinated", 1052 + "bufo-question", 1053 + "bufo-rad", 1054 + "bufo-rainbow", 1055 + "bufo-rainbow-moustache", 1056 + "bufo-raised-hand", 1057 + "bufo-ramen", 1058 + "bufo-reading", 1059 + "bufo-reads-and-analyzes-doc", 1060 + "bufo-reads-and-analyzes-doc-intensifies", 1061 + "bufo-red-flags", 1062 + "bufo-redacted", 1063 + "bufo-regret", 1064 + "bufo-remains-perturbed-from-the-void", 1065 + "bufo-remembers-bad-time", 1066 + "bufo-returns-to-the-void", 1067 + "bufo-retweet", 1068 + "bufo-reverse", 1069 + "bufo-review", 1070 + "bufo-revokes-his-approval", 1071 + "bufo-rich", 1072 + "bufo-rick", 1073 + "bufo-rides-in-style", 1074 + "bufo-riding-goose", 1075 + "bufo-riot", 1076 + "bufo-rip", 1077 + "bufo-roasted", 1078 + "bufo-robs-you", 1079 + "bufo-rocket", 1080 + "bufo-rofl", 1081 + "bufo-roll", 1082 + "bufo-roll-fast", 1083 + "bufo-roll-safe", 1084 + "bufo-roll-the-dice", 1085 + "bufo-rolling-out", 1086 + "bufo-rose", 1087 + "bufo-ross", 1088 + "bufo-royalty", 1089 + "bufo-royalty-sparkle", 1090 + "bufo-rude", 1091 + "bufo-rudolph", 1092 + "bufo-run", 1093 + "bufo-run-right", 1094 + "bufo-rush", 1095 + "bufo-sad", 1096 + "bufo-sad-baguette", 1097 + "bufo-sad-but-ok", 1098 + "bufo-sad-rain", 1099 + "bufo-sad-swinging", 1100 + "bufo-sad-vibe", 1101 + "bufo-sailor-moon", 1102 + "bufo-salad", 1103 + "bufo-salivating", 1104 + "bufo-salty", 1105 + "bufo-salute", 1106 + "bufo-same", 1107 + "bufo-santa", 1108 + "bufo-saves-hyrule", 1109 + "bufo-says-good-morning-to-test-the-waters", 1110 + "bufo-scheduled", 1111 + "bufo-science", 1112 + "bufo-science-intensifies", 1113 + "bufo-scientist", 1114 + "bufo-scientist-intensifies", 1115 + "bufo-screams-into-the-ambient-void", 1116 + "bufo-security-jacket", 1117 + "bufo-sees-what-you-did-there", 1118 + "bufo-segway", 1119 + "bufo-sends-a-demand-signal", 1120 + "bufo-sends-to-print", 1121 + "bufo-sends-you-to-the-shadow-realm", 1122 + "bufo-shakes-up-your-etch-a-sketch", 1123 + "bufo-shaking-eyes", 1124 + "bufo-shaking-head", 1125 + "bufo-shame", 1126 + "bufo-shares-his-banana", 1127 + "bufo-sheesh", 1128 + "bufo-shh", 1129 + "bufo-shh-barking-puppy", 1130 + "bufo-shifty", 1131 + "bufo-ship", 1132 + "bufo-shipit", 1133 + "bufo-shipping", 1134 + "bufo-shower", 1135 + "bufo-showing-off-baby", 1136 + "bufo-showing-off-babypilot", 1137 + "bufo-shredding", 1138 + "bufo-shrek", 1139 + "bufo-shrek-but-canonically-correct", 1140 + "bufo-shrooms", 1141 + "bufo-shrug", 1142 + "bufo-shy", 1143 + "bufo-sigh", 1144 + "bufo-silly", 1145 + "bufo-silly-goose-dance", 1146 + "bufo-simba", 1147 + "bufo-single-tear", 1148 + "bufo-sinks", 1149 + "bufo-sip", 1150 + "bufo-sipping-on-juice", 1151 + "bufo-sips-coffee", 1152 + "bufo-siren", 1153 + "bufo-sit", 1154 + "bufo-sith", 1155 + "bufo-skeledance", 1156 + "bufo-skellington", 1157 + "bufo-skellington-1", 1158 + "bufo-skiing", 1159 + "bufo-slay", 1160 + "bufo-sleep", 1161 + "bufo-slinging-bagels", 1162 + "bufo-slowly-heads-out", 1163 + "bufo-slowly-lurks-in", 1164 + "bufo-smile", 1165 + "bufo-smirk", 1166 + "bufo-smol", 1167 + "bufo-smug", 1168 + "bufo-smugo", 1169 + "bufo-snail", 1170 + "bufo-snaps-a-pic", 1171 + "bufo-snore", 1172 + "bufo-snow", 1173 + "bufo-sobbing", 1174 + "bufo-soccer", 1175 + "bufo-softball", 1176 + "bufo-sombrero", 1177 + "bufo-speaking-math", 1178 + "bufo-spider", 1179 + "bufo-spit", 1180 + "bufo-spooky-szn", 1181 + "bufo-sports", 1182 + "bufo-squad", 1183 + "bufo-squash", 1184 + "bufo-sriracha", 1185 + "bufo-stab", 1186 + "bufo-stab-murder", 1187 + "bufo-stab-reverse", 1188 + "bufo-stamp", 1189 + "bufo-standing", 1190 + "bufo-stare", 1191 + "bufo-stargazing", 1192 + "bufo-stars-in-a-old-timey-talkie", 1193 + "bufo-starstruck", 1194 + "bufo-stay-puft-marshmallow", 1195 + "bufo-steals-your-thunder", 1196 + "bufo-stick", 1197 + "bufo-stick-reverse", 1198 + "bufo-stole-caribufos-antler", 1199 + "bufo-stole-your-crunchwrap-before-you-could-finish-it", 1200 + "bufo-stoned", 1201 + "bufo-stonks", 1202 + "bufo-stonks2", 1203 + "bufo-stop", 1204 + "bufo-stopsign", 1205 + "bufo-strains-his-neck", 1206 + "bufo-strange", 1207 + "bufo-strawberry", 1208 + "bufo-strikes-a-deal", 1209 + "bufo-strikes-the-match-he's-ready-for-inferno", 1210 + "bufo-stripe", 1211 + "bufo-stuffed", 1212 + "bufo-style", 1213 + "bufo-sun-bless", 1214 + "bufo-sunny-side-up", 1215 + "bufo-surf", 1216 + "bufo-sus", 1217 + "bufo-sushi", 1218 + "bufo-sussy-eyebrows", 1219 + "bufo-sweat", 1220 + "bufo-sweep", 1221 + "bufo-sweet-dreams", 1222 + "bufo-sweet-potato", 1223 + "bufo-swims", 1224 + "bufo-sword", 1225 + "bufo-taco", 1226 + "bufo-tada", 1227 + "bufo-take-my-money", 1228 + "bufo-takes-a-bath", 1229 + "bufo-takes-bufo-give", 1230 + "bufo-takes-five-corndogs-to-the-movies-by-himself-as-his-me-time", 1231 + "bufo-takes-hotdog", 1232 + "bufo-takes-slack", 1233 + "bufo-takes-spam", 1234 + "bufo-takes-your-approval", 1235 + "bufo-takes-your-boba", 1236 + "bufo-takes-your-bufo-taco", 1237 + "bufo-takes-your-burrito", 1238 + "bufo-takes-your-copilot", 1239 + "bufo-takes-your-fud-away", 1240 + "bufo-takes-your-golden-handcuffs", 1241 + "bufo-takes-your-incident", 1242 + "bufo-takes-your-nose", 1243 + "bufo-takes-your-pizza", 1244 + "bufo-takes-yubikey", 1245 + "bufo-takes-zoom", 1246 + "bufo-talks-to-brick-wall", 1247 + "bufo-tapioca-pearl", 1248 + "bufo-tea", 1249 + "bufo-teal", 1250 + "bufo-tears-of-joy", 1251 + "bufo-tense", 1252 + "bufo-tequila", 1253 + "bufo-thanks", 1254 + "bufo-thanks-bufo-for-thanking-bufo", 1255 + "bufo-thanks-the-sr-bufo-for-their-wisdom", 1256 + "bufo-thanks-you-for-the-approval", 1257 + "bufo-thanks-you-for-the-bufo", 1258 + "bufo-thanks-you-for-the-comment", 1259 + "bufo-thanks-you-for-the-new-bufo", 1260 + "bufo-thanks-you-for-your-issue", 1261 + "bufo-thanks-you-for-your-pr", 1262 + "bufo-thanks-you-for-your-service", 1263 + "bufo-thanksgiving", 1264 + "bufo-thanos", 1265 + "bufo-thats-a-knee-slapper", 1266 + "bufo-the-builder", 1267 + "bufo-the-crying-osha-compliant-builder", 1268 + "bufo-the-osha-compliant-builder", 1269 + "bufo-think", 1270 + "bufo-thinking", 1271 + "bufo-thinking-about-holidays", 1272 + "bufo-thinks-about-a11y", 1273 + "bufo-thinks-about-azure", 1274 + "bufo-thinks-about-azure-front-door", 1275 + "bufo-thinks-about-azure-front-door-intensifies", 1276 + "bufo-thinks-about-cheeky-nandos", 1277 + "bufo-thinks-about-chocolate", 1278 + "bufo-thinks-about-climbing", 1279 + "bufo-thinks-about-docs", 1280 + "bufo-thinks-about-fishsticks", 1281 + "bufo-thinks-about-mountains", 1282 + "bufo-thinks-about-omelette", 1283 + "bufo-thinks-about-pancakes", 1284 + "bufo-thinks-about-quarter", 1285 + "bufo-thinks-about-redis", 1286 + "bufo-thinks-about-rubberduck", 1287 + "bufo-thinks-about-steak", 1288 + "bufo-thinks-about-steakholder", 1289 + "bufo-thinks-about-teams", 1290 + "bufo-thinks-about-telemetry", 1291 + "bufo-thinks-about-terraform", 1292 + "bufo-thinks-about-ufo", 1293 + "bufo-thinks-about-vacation", 1294 + "bufo-thinks-he-gets-paid-too-much-to-work-here", 1295 + "bufo-thinks-of-shamenun", 1296 + "bufo-thinks-this-is-a-total-bop", 1297 + "bufo-this", 1298 + "bufo-this-is-fine", 1299 + "bufo-this2", 1300 + "bufo-thonk", 1301 + "bufo-thonks-from-the-void", 1302 + "bufo-threatens-to-hit-you-with-the-chancla-and-he-means-it", 1303 + "bufo-threatens-to-thwack-you-with-a-slipper-and-he-means-it", 1304 + "bufo-throws-brick", 1305 + "bufo-thumbsup", 1306 + "bufo-thunk", 1307 + "bufo-thwack", 1308 + "bufo-timeout", 1309 + "bufo-tin-foil-hat", 1310 + "bufo-tin-foil-hat2", 1311 + "bufo-tips-hat", 1312 + "bufo-tired", 1313 + "bufo-tired-of-rooting-for-the-anti-hero", 1314 + "bufo-tired-yes", 1315 + "bufo-toad", 1316 + "bufo-tofu", 1317 + "bufo-toilet-rocket", 1318 + "bufo-tomato", 1319 + "bufo-tongue", 1320 + "bufo-too-many-pings", 1321 + "bufo-took-too-much", 1322 + "bufo-tooth", 1323 + "bufo-tophat", 1324 + "bufo-tortoise", 1325 + "bufo-torus", 1326 + "bufo-trailhead", 1327 + "bufo-train", 1328 + "bufo-transfixed", 1329 + "bufo-transmutes-reality", 1330 + "bufo-trash-can", 1331 + "bufo-travels", 1332 + "bufo-tries-some-yummy-yummy-crossplane", 1333 + "bufo-tries-to-fight-you-but-his-arms-are-too-short-so-count-yourself-lucky", 1334 + "bufo-tries-to-hug-you-back-but-his-arms-are-too-short", 1335 + "bufo-tries-to-hug-you-but-his-arms-are-too-short", 1336 + "bufo-triple-vaccinated", 1337 + "bufo-tripping", 1338 + "bufo-trying-to-relax-while-procrastinating-but-its-not-working", 1339 + "bufo-turns-the-tables", 1340 + "bufo-tux", 1341 + "bufo-typing", 1342 + "bufo-u-dead", 1343 + "bufo-ufo", 1344 + "bufo-ugh", 1345 + "bufo-uh-okay-i-guess-so", 1346 + "bufo-uhhh", 1347 + "bufo-underpaid-postage-at-usps-and-now-they're-coming-after-him-for-the-money-he-owes", 1348 + "bufo-unicorn", 1349 + "bufo-universe", 1350 + "bufo-unlocked-transdimensional-travel-while-in-the-void", 1351 + "bufo-uno", 1352 + "bufo-upvote", 1353 + "bufo-uses-100-percent-of-his-brain", 1354 + "bufo-uwu", 1355 + "bufo-vaccinated", 1356 + "bufo-vaccinates-you", 1357 + "bufo-vampire", 1358 + "bufo-venom", 1359 + "bufo-ventilator", 1360 + "bufo-very-angry", 1361 + "bufo-vibe", 1362 + "bufo-vibe-dance", 1363 + "bufo-vomit", 1364 + "bufo-voted", 1365 + "bufo-waddle", 1366 + "bufo-waiting-for-aws-to-deep-archive-our-data", 1367 + "bufo-waiting-for-azure", 1368 + "bufo-waits-in-queue", 1369 + "bufo-waldo", 1370 + "bufo-walk-away", 1371 + "bufo-wallop", 1372 + "bufo-wants-a-refund", 1373 + "bufo-wants-to-have-a-calm-and-civilized-conversation-with-you", 1374 + "bufo-wants-to-know-your-spaghetti-policy-at-the-movies", 1375 + "bufo-wants-to-return-his-vacuum-that-he-bought-at-costco-four-years-ago-for-a-full-refund", 1376 + "bufo-wants-you-to-buy-his-crypto", 1377 + "bufo-wards-off-the-evil-spirits", 1378 + "bufo-warhol", 1379 + "bufo-was-eavesdropping-and-got-offended-by-your-convo-but-now-has-to-pretend-he-didnt-hear-you", 1380 + "bufo-was-in-paris", 1381 + "bufo-wat", 1382 + "bufo-watches-from-a-distance", 1383 + "bufo-watches-the-rain", 1384 + "bufo-watching-the-clock", 1385 + "bufo-watermelon", 1386 + "bufo-wave", 1387 + "bufo-waves-hello-from-the-void", 1388 + "bufo-wears-a-paper-crown", 1389 + "bufo-wears-the-cone-of-shame", 1390 + "bufo-wedding", 1391 + "bufo-welcome", 1392 + "bufo-welp", 1393 + "bufo-whack", 1394 + "bufo-what-are-you-doing-with-that", 1395 + "bufo-what-did-you-just-say", 1396 + "bufo-what-have-i-done", 1397 + "bufo-what-have-you-done", 1398 + "bufo-what-if", 1399 + "bufo-whatever", 1400 + "bufo-whew", 1401 + "bufo-whisky", 1402 + "bufo-who-me", 1403 + "bufo-wholesome", 1404 + "bufo-why-must-it-all-be-this-way", 1405 + "bufo-why-must-it-be-this-way", 1406 + "bufo-wicked", 1407 + "bufo-wide", 1408 + "bufo-wider-01", 1409 + "bufo-wider-02", 1410 + "bufo-wider-03", 1411 + "bufo-wider-04", 1412 + "bufo-wields-mjolnir", 1413 + "bufo-wields-the-hylian-shield", 1414 + "bufo-will-miss-you", 1415 + "bufo-will-never-walk-cornelia-street-again", 1416 + "bufo-will-not-be-going-to-space-today", 1417 + "bufo-wine", 1418 + "bufo-wink", 1419 + "bufo-wishes-you-a-happy-valentines-day", 1420 + "bufo-with-a-drive-by-hot-take", 1421 + "bufo-with-a-fresh-do", 1422 + "bufo-with-a-pearl-earring", 1423 + "bufo-wizard", 1424 + "bufo-wizard-magic-charge", 1425 + "bufo-wonders-if-deliciousness-of-this-cheese-is-worth-the-pain-his-lactose-intolerance-will-cause", 1426 + "bufo-workin-up-a-sweat-after-eating-a-wendys-double-loaded-double-baked-baked-potato-during-summer", 1427 + "bufo-worldstar", 1428 + "bufo-worried", 1429 + "bufo-worry", 1430 + "bufo-worry-coffee", 1431 + "bufo-would-like-a-bite-of-your-cookie", 1432 + "bufo-writes-a-doc", 1433 + "bufo-wtf", 1434 + "bufo-wut", 1435 + "bufo-yah", 1436 + "bufo-yay", 1437 + "bufo-yay-awkward-eyes", 1438 + "bufo-yay-confetti", 1439 + "bufo-yay-judge", 1440 + "bufo-yayy", 1441 + "bufo-yeehaw", 1442 + "bufo-yells-at-old-bufo", 1443 + "bufo-yes", 1444 + "bufo-yismail", 1445 + "bufo-you-sure-about-that", 1446 + "bufo-yugioh", 1447 + "bufo-yummy", 1448 + "bufo-zoom", 1449 + "bufo-zoom-right", 1450 + "bufo's-a-gamer-girl-but-specifically-nyt-games", 1451 + "bufo+1", 1452 + "bufobot", 1453 + "bufochu", 1454 + "bufocopter", 1455 + "bufoda", 1456 + "bufodile", 1457 + "bufofoop", 1458 + "bufoheimer", 1459 + "bufohub", 1460 + "bufolatro", 1461 + "bufoling", 1462 + "bufolo", 1463 + "bufolta", 1464 + "bufonana", 1465 + "bufone", 1466 + "bufonomical", 1467 + "bufopilot", 1468 + "bufopoof", 1469 + "buforang", 1470 + "buforce-be-with-you", 1471 + "buforead", 1472 + "buforever", 1473 + "bufos-got-your-back", 1474 + "bufos-in-love", 1475 + "bufos-jumping-on-the-bed", 1476 + "bufos-lips-are-sealed", 1477 + "bufovacado", 1478 + "bufowhirl", 1479 + "bufrogu", 1480 + "but-wait-theres-bufo", 1481 + "child-bufo-only-has-deku-sticks-to-save-hyrule", 1482 + "chonky-bufo-wants-to-be-held", 1483 + "christmas-bufo-on-a-goose", 1484 + "circle-of-bufo", 1485 + "confused-math-bufo", 1486 + "constipated-bufo-is-trying-his-hardest", 1487 + "copper-bufo", 1488 + "corrupted-bufo", 1489 + "count-bufo", 1490 + "daily-dose-of-bufo-vitamins", 1491 + "dalmatian-bufo", 1492 + "death-by-a-thousand-bufo-stabs", 1493 + "doctor-bufo", 1494 + "dont-make-bufo-tap-the-sign", 1495 + "double-bufo-sideeye", 1496 + "egg-bufo", 1497 + "eggplant-bufo", 1498 + "et-tu-bufo", 1499 + "everybody-loves-bufo", 1500 + "existential-bufo", 1501 + "feelsgoodbufo", 1502 + "fix-it-bufo", 1503 + "friendly-neighborhood-bufo", 1504 + "future-bufos", 1505 + "get-in-lets-bufo", 1506 + "get-out-of-bufos-swamp", 1507 + "ghost-bufo-of-future-past-is-disappointed-in-your-lack-of-foresight", 1508 + "gold-bufo", 1509 + "good-news-bufo-offers-suppository", 1510 + "google-sheet-bufo", 1511 + "great-white-bufo", 1512 + "happy-bufo-brings-you-a-deescalation-coffee", 1513 + "happy-bufo-brings-you-a-deescalation-tea", 1514 + "heavy-is-the-bufo-that-wears-the-crown", 1515 + "holiday-bufo-offers-you-a-candy-cane", 1516 + "house-of-bufo", 1517 + "i-dont-trust-bufo", 1518 + "i-heart-bufo", 1519 + "i-think-you-should-leave-with-bufo", 1520 + "if-bufo-fits-bufo-sits", 1521 + "interdimensional-bufo-rests-atop-the-terrarium-of-existence", 1522 + "it-takes-a-bufo-to-know-a-bufo", 1523 + "its-been-such-a-long-day-that-bufo-doesnt-really-care-anymore", 1524 + "just-a-bunch-of-bufos", 1525 + "just-hear-bufo-out-for-a-sec", 1526 + "kermit-the-bufo", 1527 + "king-bufo", 1528 + "kirbufo", 1529 + "le-bufo", 1530 + "live-laugh-bufo", 1531 + "loch-ness-bufo", 1532 + "looks-good-to-bufo", 1533 + "low-fidelity-bufo-cant-believe-youve-done-this", 1534 + "low-fidelity-bufo-concerned", 1535 + "low-fidelity-bufo-excited", 1536 + "low-fidelity-bufo-gets-whiplash", 1537 + "m-bufo", 1538 + "maam-this-is-a-bufo", 1539 + "many-bufos", 1540 + "maybe-a-bufo-bigfoot", 1541 + "mega-bufo", 1542 + "mrs-bufo", 1543 + "my-name-is-buford-and-i-am-bufo's-father", 1544 + "nobufo", 1545 + "not-bufo", 1546 + "nothing-inauthentic-bout-this-bufo-yeah-hes-the-real-thing-baby", 1547 + "old-bufo-yells-at-cloud", 1548 + "old-bufo-yells-at-hubble", 1549 + "old-man-yells-at-bufo", 1550 + "old-man-yells-at-old-bufo", 1551 + "one-of-101-bufos", 1552 + "our-bufo-is-in-another-castle", 1553 + "paper-bufo", 1554 + "party-bufo", 1555 + "pixel-bufo", 1556 + "planet-bufo", 1557 + "please-converse-using-only-bufo", 1558 + "poison-dart-bufo", 1559 + "pour-one-out-for-bufo", 1560 + "press-x-to-bufo", 1561 + "princebufo", 1562 + "proud-bufo-is-excited", 1563 + "radioactive-bufo", 1564 + "sad-bufo", 1565 + "safe-driver-bufo", 1566 + "se%C3%B1or-bufo", 1567 + "sen%CC%83or-bufo", 1568 + "shiny-bufo", 1569 + "shut-up-and-take-my-bufo", 1570 + "silver-bufo", 1571 + "sir-bufo-esquire", 1572 + "sir-this-is-a-bufo", 1573 + "sleepy-bufo", 1574 + "smol-bufo-feels-blessed", 1575 + "smol-bufo-has-a-smol-pull-request-that-needs-reviews-and-he-promises-it-will-only-take-a-minute", 1576 + "so-bufoful", 1577 + "spider-bufo", 1578 + "spotify-wrapped-reminded-bufo-his-listening-patterns-are-a-little-unhinged", 1579 + "super-bufo", 1580 + "super-bufo-bros", 1581 + "tabufo", 1582 + "teamwork-makes-the-bufo-work", 1583 + "ted-bufo", 1584 + "the_bufo_formerly_know_as_froge", 1585 + "the-bufo-nightmare-before-christmas", 1586 + "the-bufo-we-deserve", 1587 + "the-bufos-new-groove", 1588 + "the-creation-of-bufo", 1589 + "the-more-you-bufo", 1590 + "the-pinkest-bufo-there-ever-was", 1591 + "theres-a-bufo-for-that", 1592 + "this-8-dollar-starbucks-drink-isnt-helping-bufo-feel-any-better", 1593 + "this-is-bufo", 1594 + "this-will-be-bufos-little-secret", 1595 + "triumphant-bufo", 1596 + "tsa-bufo-gropes-you", 1597 + "two-bufos-beefin", 1598 + "up-and-to-the-bufo", 1599 + "vin-bufo", 1600 + "vintage-bufo", 1601 + "whatever-youre-doing-its-attracting-the-bufos", 1602 + "when-bufo-falls-in-love", 1603 + "whenlifegetsatbufo", 1604 + "with-friends-like-this-bufo-doesnt-need-enemies", 1605 + "wreck-it-bufo", 1606 + "wrong-frog", 1607 + "yay-bufo-1", 1608 + "yay-bufo-2", 1609 + "yay-bufo-3", 1610 + "yay-bufo-4", 1611 + "you-have-awoken-the-bufo", 1612 + "you-have-exquisite-taste-in-bufo", 1613 + "you-left-your-typewriter-at-bufos-apartment" 1614 + ]
+8
site/favicon.svg
···
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"> 2 + <!-- Outer ring --> 3 + <circle cx="16" cy="16" r="14" fill="none" stroke="#4a9eff" stroke-width="2"/> 4 + <!-- Inner status dot --> 5 + <circle cx="16" cy="16" r="8" fill="#4a9eff"/> 6 + <!-- Small highlight to give it depth --> 7 + <circle cx="18" cy="14" r="3" fill="#6bb2ff" opacity="0.7"/> 8 + </svg>
+17
site/fly.toml
···
··· 1 + app = "quickslice-status" 2 + primary_region = "ewr" 3 + 4 + [build] 5 + dockerfile = "Dockerfile" 6 + 7 + [http_service] 8 + internal_port = 8000 9 + force_https = true 10 + auto_stop_machines = "stop" 11 + auto_start_machines = true 12 + min_machines_running = 0 13 + 14 + [[vm]] 15 + cpu_kind = "shared" 16 + cpus = 1 17 + memory_mb = 256
+49
site/index.html
···
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>status</title> 7 + <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 8 + <link rel="stylesheet" href="/styles.css"> 9 + <script src="https://cdn.jsdelivr.net/gh/bigmoves/quickslice@main/quickslice-client-js/dist/quickslice-client.min.js"></script> 10 + </head> 11 + <body> 12 + <div id="app"> 13 + <header> 14 + <h1 id="page-title">status</h1> 15 + <nav> 16 + <a href="/" id="nav-home" class="nav-btn" aria-label="home" title="home"> 17 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18 + <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path> 19 + <polyline points="9 22 9 12 15 12 15 22"></polyline> 20 + </svg> 21 + </a> 22 + <a href="/feed" id="nav-feed" class="nav-btn" aria-label="global feed" title="global feed"> 23 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 24 + <circle cx="12" cy="12" r="10"></circle> 25 + <line x1="2" y1="12" x2="22" y2="12"></line> 26 + <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path> 27 + </svg> 28 + </a> 29 + <button id="settings-btn" class="nav-btn hidden" aria-label="settings" title="settings"> 30 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 31 + <circle cx="12" cy="12" r="3"></circle> 32 + <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path> 33 + </svg> 34 + </button> 35 + <button id="theme-toggle" aria-label="toggle theme"> 36 + <span class="sun">☀</span> 37 + <span class="moon">☾</span> 38 + </button> 39 + </nav> 40 + </header> 41 + 42 + <main id="main-content"> 43 + <div class="center">loading...</div> 44 + </main> 45 + </div> 46 + 47 + <script src="/app.js"></script> 48 + </body> 49 + </html>
+791
site/styles.css
···
··· 1 + :root { 2 + --bg: #0a0a0a; 3 + --bg-card: #1a1a1a; 4 + --text: #ffffff; 5 + --text-secondary: #888; 6 + --accent: #4a9eff; 7 + --border: #2a2a2a; 8 + --radius: 12px; 9 + --font-family: ui-monospace, "SF Mono", Monaco, monospace; 10 + } 11 + 12 + [data-theme="light"] { 13 + --bg: #ffffff; 14 + --bg-card: #f5f5f5; 15 + --text: #1a1a1a; 16 + --text-secondary: #666; 17 + --border: #e0e0e0; 18 + } 19 + 20 + * { 21 + margin: 0; 22 + padding: 0; 23 + box-sizing: border-box; 24 + } 25 + 26 + /* Theme-aware scrollbars */ 27 + ::-webkit-scrollbar { 28 + width: 8px; 29 + height: 8px; 30 + } 31 + 32 + ::-webkit-scrollbar-track { 33 + background: var(--bg); 34 + } 35 + 36 + ::-webkit-scrollbar-thumb { 37 + background: var(--border); 38 + border-radius: 4px; 39 + } 40 + 41 + ::-webkit-scrollbar-thumb:hover { 42 + background: var(--text-secondary); 43 + } 44 + 45 + /* Firefox */ 46 + * { 47 + scrollbar-width: thin; 48 + scrollbar-color: var(--border) var(--bg); 49 + } 50 + 51 + body { 52 + font-family: var(--font-family); 53 + background: var(--bg); 54 + color: var(--text); 55 + line-height: 1.6; 56 + min-height: 100vh; 57 + } 58 + 59 + #app { 60 + max-width: 600px; 61 + margin: 0 auto; 62 + padding: 2rem 1rem; 63 + } 64 + 65 + header { 66 + display: flex; 67 + justify-content: space-between; 68 + align-items: center; 69 + margin-bottom: 2rem; 70 + padding-bottom: 1rem; 71 + border-bottom: 1px solid var(--border); 72 + } 73 + 74 + header h1 { 75 + font-size: 1.5rem; 76 + font-weight: 600; 77 + } 78 + 79 + nav { 80 + display: flex; 81 + gap: 1rem; 82 + align-items: center; 83 + } 84 + 85 + nav a { 86 + color: var(--text-secondary); 87 + text-decoration: none; 88 + } 89 + 90 + nav a:hover { 91 + color: var(--accent); 92 + } 93 + 94 + .nav-btn { 95 + display: flex; 96 + align-items: center; 97 + justify-content: center; 98 + padding: 0.5rem; 99 + border-radius: 8px; 100 + transition: background 0.15s, color 0.15s; 101 + color: var(--text-secondary); 102 + background: none; 103 + border: none; 104 + cursor: pointer; 105 + } 106 + 107 + .nav-btn:hover { 108 + background: var(--bg-card); 109 + color: var(--accent); 110 + } 111 + 112 + .nav-btn.active { 113 + color: var(--accent); 114 + } 115 + 116 + .nav-btn svg { 117 + display: block; 118 + } 119 + 120 + #theme-toggle { 121 + background: none; 122 + border: 1px solid var(--border); 123 + border-radius: 8px; 124 + padding: 0.5rem; 125 + cursor: pointer; 126 + font-size: 1rem; 127 + } 128 + 129 + #theme-toggle .sun { display: none; } 130 + #theme-toggle .moon { display: inline; color: var(--text); } 131 + [data-theme="light"] #theme-toggle .sun { display: inline; color: var(--text); } 132 + [data-theme="light"] #theme-toggle .moon { display: none; } 133 + 134 + .hidden { display: none !important; } 135 + .center { text-align: center; padding: 2rem; } 136 + 137 + /* Login form */ 138 + #login-form { 139 + display: flex; 140 + gap: 0.5rem; 141 + margin-top: 1rem; 142 + justify-content: center; 143 + } 144 + 145 + #login-form input { 146 + padding: 0.75rem 1rem; 147 + border: 1px solid var(--border); 148 + border-radius: var(--radius); 149 + background: var(--bg-card); 150 + color: var(--text); 151 + font-family: inherit; 152 + font-size: 1rem; 153 + width: 200px; 154 + } 155 + 156 + #login-form button, button[type="submit"] { 157 + padding: 0.75rem 1.5rem; 158 + background: var(--accent); 159 + color: white; 160 + border: none; 161 + border-radius: var(--radius); 162 + cursor: pointer; 163 + font-family: inherit; 164 + font-size: 1rem; 165 + } 166 + 167 + #login-form button:hover, button[type="submit"]:hover { 168 + opacity: 0.9; 169 + } 170 + 171 + /* Profile card */ 172 + .profile-card { 173 + background: var(--bg-card); 174 + border: 1px solid var(--border); 175 + border-radius: var(--radius); 176 + padding: 2rem; 177 + margin-bottom: 1.5rem; 178 + } 179 + 180 + .current-status { 181 + display: flex; 182 + flex-direction: column; 183 + align-items: center; 184 + gap: 1rem; 185 + text-align: center; 186 + } 187 + 188 + .big-emoji { 189 + font-size: 4rem; 190 + line-height: 1; 191 + } 192 + 193 + .big-emoji img { 194 + width: 4rem; 195 + height: 4rem; 196 + object-fit: contain; 197 + } 198 + 199 + .status-info { 200 + display: flex; 201 + flex-direction: column; 202 + gap: 0.25rem; 203 + } 204 + 205 + #current-text { 206 + font-size: 1.25rem; 207 + } 208 + 209 + .meta { 210 + color: var(--text-secondary); 211 + font-size: 0.875rem; 212 + } 213 + 214 + /* Status form */ 215 + .status-form { 216 + background: var(--bg-card); 217 + border: 1px solid var(--border); 218 + border-radius: var(--radius); 219 + padding: 1rem; 220 + margin-bottom: 1.5rem; 221 + } 222 + 223 + .emoji-input-row { 224 + display: flex; 225 + gap: 0.5rem; 226 + margin-bottom: 0.75rem; 227 + } 228 + 229 + .emoji-input-row input { 230 + flex: 1; 231 + padding: 0.75rem; 232 + border: 1px solid var(--border); 233 + border-radius: 8px; 234 + background: var(--bg); 235 + color: var(--text); 236 + font-family: inherit; 237 + font-size: 1rem; 238 + } 239 + 240 + #emoji-input { 241 + max-width: 150px; 242 + } 243 + 244 + .form-actions { 245 + display: flex; 246 + gap: 0.5rem; 247 + justify-content: flex-end; 248 + } 249 + 250 + .form-actions select { 251 + padding: 0.75rem; 252 + border: 1px solid var(--border); 253 + border-radius: 8px; 254 + background: var(--bg); 255 + color: var(--text); 256 + font-family: inherit; 257 + } 258 + 259 + .custom-datetime { 260 + padding: 0.75rem; 261 + border: 1px solid var(--border); 262 + border-radius: 8px; 263 + background: var(--bg); 264 + color: var(--text); 265 + font-family: inherit; 266 + } 267 + 268 + /* History */ 269 + .history { 270 + margin-bottom: 2rem; 271 + } 272 + 273 + .history h2 { 274 + font-size: 0.875rem; 275 + text-transform: uppercase; 276 + letter-spacing: 0.05em; 277 + color: var(--text-secondary); 278 + margin-bottom: 1rem; 279 + } 280 + 281 + #history-list { 282 + display: flex; 283 + flex-direction: column; 284 + gap: 0.75rem; 285 + } 286 + 287 + /* Feed list */ 288 + .feed-list { 289 + display: flex; 290 + flex-direction: column; 291 + gap: 1rem; 292 + } 293 + 294 + /* Status item (used in both history and feed) */ 295 + .status-item { 296 + display: flex; 297 + gap: 1rem; 298 + padding: 1rem; 299 + background: var(--bg-card); 300 + border: 1px solid var(--border); 301 + border-radius: var(--radius); 302 + align-items: flex-start; 303 + } 304 + 305 + .status-item:hover { 306 + border-color: var(--accent); 307 + } 308 + 309 + .status-item .emoji { 310 + font-size: 1.5rem; 311 + line-height: 1; 312 + flex-shrink: 0; 313 + } 314 + 315 + .status-item .emoji img { 316 + width: 1.5rem; 317 + height: 1.5rem; 318 + object-fit: contain; 319 + } 320 + 321 + .status-item .content { 322 + flex: 1; 323 + min-width: 0; 324 + } 325 + 326 + .status-item .author { 327 + color: var(--text-secondary); 328 + font-weight: 600; 329 + text-decoration: none; 330 + } 331 + 332 + .status-item .author:hover { 333 + color: var(--accent); 334 + } 335 + 336 + .status-item .text { 337 + margin-left: 0.5rem; 338 + } 339 + 340 + .status-item .time { 341 + display: block; 342 + font-size: 0.875rem; 343 + color: var(--text-secondary); 344 + margin-top: 0.25rem; 345 + } 346 + 347 + .delete-btn { 348 + background: transparent; 349 + border: none; 350 + color: var(--text-secondary); 351 + cursor: pointer; 352 + padding: 0.25rem; 353 + border-radius: 4px; 354 + opacity: 0; 355 + transition: opacity 0.15s, color 0.15s; 356 + flex-shrink: 0; 357 + } 358 + 359 + .status-item:hover .delete-btn { 360 + opacity: 1; 361 + } 362 + 363 + .delete-btn:hover { 364 + color: #e74c3c; 365 + } 366 + 367 + /* Logout */ 368 + .logout-btn { 369 + display: block; 370 + margin: 0 auto; 371 + padding: 0.5rem 1rem; 372 + background: none; 373 + border: 1px solid var(--border); 374 + border-radius: 8px; 375 + color: var(--text-secondary); 376 + cursor: pointer; 377 + font-family: inherit; 378 + } 379 + 380 + .logout-btn:hover { 381 + border-color: var(--text); 382 + color: var(--text); 383 + } 384 + 385 + /* Load more */ 386 + #load-more-btn { 387 + padding: 0.75rem 1.5rem; 388 + background: var(--bg-card); 389 + border: 1px solid var(--border); 390 + border-radius: var(--radius); 391 + color: var(--text); 392 + cursor: pointer; 393 + font-family: inherit; 394 + } 395 + 396 + #load-more-btn:hover { 397 + border-color: var(--accent); 398 + } 399 + 400 + /* Emoji trigger button */ 401 + .emoji-trigger { 402 + width: 3rem; 403 + height: 3rem; 404 + border: none; 405 + border-radius: 8px; 406 + background: transparent; 407 + cursor: pointer; 408 + display: flex; 409 + align-items: center; 410 + justify-content: center; 411 + font-size: 1.75rem; 412 + flex-shrink: 0; 413 + } 414 + 415 + .emoji-trigger:hover { 416 + background: var(--bg-card); 417 + } 418 + 419 + .emoji-trigger img { 420 + width: 2.5rem; 421 + height: 2.5rem; 422 + object-fit: contain; 423 + } 424 + 425 + /* Emoji picker overlay */ 426 + .emoji-picker-overlay { 427 + position: fixed; 428 + inset: 0; 429 + background: rgba(0, 0, 0, 0.7); 430 + display: flex; 431 + align-items: center; 432 + justify-content: center; 433 + z-index: 1000; 434 + padding: 1rem; 435 + } 436 + 437 + .emoji-picker { 438 + background: var(--bg-card); 439 + border: 1px solid var(--border); 440 + border-radius: var(--radius); 441 + width: 100%; 442 + max-width: 600px; 443 + height: 90vh; 444 + max-height: 700px; 445 + display: flex; 446 + flex-direction: column; 447 + overflow: hidden; 448 + } 449 + 450 + .emoji-picker-header { 451 + display: flex; 452 + justify-content: space-between; 453 + align-items: center; 454 + padding: 1rem; 455 + border-bottom: 1px solid var(--border); 456 + } 457 + 458 + .emoji-picker-header h3 { 459 + font-size: 1rem; 460 + font-weight: 600; 461 + } 462 + 463 + .emoji-picker-close { 464 + background: none; 465 + border: none; 466 + color: var(--text-secondary); 467 + cursor: pointer; 468 + font-size: 1.25rem; 469 + padding: 0.25rem; 470 + } 471 + 472 + .emoji-picker-close:hover { 473 + color: var(--text); 474 + } 475 + 476 + .emoji-search { 477 + margin: 0.75rem; 478 + padding: 0.5rem 0.75rem; 479 + border: 1px solid var(--border); 480 + border-radius: 8px; 481 + background: var(--bg); 482 + color: var(--text); 483 + font-family: inherit; 484 + font-size: 0.875rem; 485 + } 486 + 487 + .emoji-categories { 488 + display: flex; 489 + gap: 0.25rem; 490 + padding: 0 0.75rem; 491 + overflow-x: auto; 492 + flex-shrink: 0; 493 + } 494 + 495 + .category-btn { 496 + padding: 0.5rem; 497 + border: none; 498 + background: none; 499 + cursor: pointer; 500 + font-size: 1.25rem; 501 + border-radius: 8px; 502 + opacity: 0.5; 503 + transition: opacity 0.15s; 504 + } 505 + 506 + .category-btn:hover, .category-btn.active { 507 + opacity: 1; 508 + background: var(--bg); 509 + } 510 + 511 + .emoji-grid { 512 + padding: 0.75rem; 513 + display: grid; 514 + grid-template-columns: repeat(auto-fill, minmax(48px, 1fr)); 515 + gap: 0.25rem; 516 + overflow-y: auto; 517 + flex: 1; 518 + min-height: 200px; 519 + align-content: start; 520 + } 521 + 522 + .emoji-grid.bufo-grid { 523 + grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); 524 + gap: 0.5rem; 525 + } 526 + 527 + .emoji-btn { 528 + padding: 0.5rem; 529 + border: none; 530 + background: none; 531 + cursor: pointer; 532 + font-size: 1.5rem; 533 + border-radius: 8px; 534 + transition: background 0.15s; 535 + } 536 + 537 + .emoji-btn:hover { 538 + background: var(--bg); 539 + } 540 + 541 + /* Consistent sizing for mixed emoji/bufo grids (frequent tab) */ 542 + .emoji-grid .emoji-btn { 543 + width: 48px; 544 + height: 48px; 545 + display: flex; 546 + align-items: center; 547 + justify-content: center; 548 + font-size: 1.75rem; 549 + } 550 + 551 + .bufo-btn { 552 + padding: 0.25rem; 553 + } 554 + 555 + .bufo-grid .bufo-btn { 556 + width: 64px; 557 + height: 64px; 558 + } 559 + 560 + .bufo-btn img { 561 + width: 100%; 562 + height: 100%; 563 + max-width: 48px; 564 + max-height: 48px; 565 + object-fit: contain; 566 + } 567 + 568 + .loading { 569 + grid-column: 1 / -1; 570 + text-align: center; 571 + color: var(--text-secondary); 572 + padding: 2rem; 573 + } 574 + 575 + .no-results { 576 + grid-column: 1 / -1; 577 + text-align: center; 578 + color: var(--text-secondary); 579 + padding: 2rem; 580 + } 581 + 582 + /* Custom emoji input */ 583 + .custom-emoji-input { 584 + grid-column: 1 / -1; 585 + display: flex; 586 + gap: 0.5rem; 587 + margin-bottom: 1rem; 588 + } 589 + 590 + .custom-emoji-input input { 591 + flex: 1; 592 + padding: 0.5rem 0.75rem; 593 + border: 1px solid var(--border); 594 + border-radius: 8px; 595 + background: var(--bg); 596 + color: var(--text); 597 + font-family: inherit; 598 + } 599 + 600 + .custom-emoji-input button { 601 + padding: 0.5rem 1rem; 602 + background: var(--accent); 603 + color: white; 604 + border: none; 605 + border-radius: 8px; 606 + cursor: pointer; 607 + font-family: inherit; 608 + } 609 + 610 + .custom-emoji-preview { 611 + grid-column: 1 / -1; 612 + display: flex; 613 + justify-content: center; 614 + min-height: 80px; 615 + align-items: center; 616 + } 617 + 618 + .bufo-helper { 619 + padding: 0.75rem; 620 + text-align: center; 621 + border-top: 1px solid var(--border); 622 + } 623 + 624 + .bufo-helper a { 625 + color: var(--accent); 626 + font-size: 0.875rem; 627 + } 628 + 629 + /* Settings Modal */ 630 + .settings-overlay { 631 + position: fixed; 632 + top: 0; 633 + left: 0; 634 + right: 0; 635 + bottom: 0; 636 + background: rgba(0, 0, 0, 0.7); 637 + display: flex; 638 + align-items: center; 639 + justify-content: center; 640 + z-index: 1000; 641 + padding: 1rem; 642 + } 643 + 644 + .settings-modal { 645 + background: var(--bg-card); 646 + border: 1px solid var(--border); 647 + border-radius: var(--radius); 648 + width: 100%; 649 + max-width: 400px; 650 + display: flex; 651 + flex-direction: column; 652 + } 653 + 654 + .settings-header { 655 + display: flex; 656 + justify-content: space-between; 657 + align-items: center; 658 + padding: 1rem; 659 + border-bottom: 1px solid var(--border); 660 + } 661 + 662 + .settings-header h3 { 663 + font-size: 1.1rem; 664 + font-weight: 500; 665 + } 666 + 667 + .settings-close { 668 + background: none; 669 + border: none; 670 + color: var(--text-secondary); 671 + cursor: pointer; 672 + font-size: 1.25rem; 673 + padding: 0.25rem; 674 + } 675 + 676 + .settings-close:hover { 677 + color: var(--text); 678 + } 679 + 680 + .settings-content { 681 + padding: 1rem; 682 + display: flex; 683 + flex-direction: column; 684 + gap: 1.25rem; 685 + } 686 + 687 + .setting-group { 688 + display: flex; 689 + flex-direction: column; 690 + gap: 0.5rem; 691 + } 692 + 693 + .setting-group label { 694 + font-size: 0.875rem; 695 + color: var(--text-secondary); 696 + } 697 + 698 + .setting-group select { 699 + padding: 0.75rem; 700 + border: 1px solid var(--border); 701 + border-radius: 8px; 702 + background: var(--bg); 703 + color: var(--text); 704 + font-family: inherit; 705 + font-size: 1rem; 706 + } 707 + 708 + .color-picker { 709 + display: flex; 710 + flex-wrap: wrap; 711 + gap: 0.5rem; 712 + align-items: center; 713 + } 714 + 715 + .color-btn { 716 + width: 32px; 717 + height: 32px; 718 + border-radius: 50%; 719 + border: 2px solid transparent; 720 + cursor: pointer; 721 + transition: border-color 0.15s, transform 0.15s; 722 + } 723 + 724 + .color-btn:hover { 725 + transform: scale(1.1); 726 + } 727 + 728 + .color-btn.active { 729 + border-color: var(--text); 730 + } 731 + 732 + .custom-color-input { 733 + width: 32px; 734 + height: 32px; 735 + border: none; 736 + border-radius: 50%; 737 + cursor: pointer; 738 + background: none; 739 + padding: 0; 740 + } 741 + 742 + .custom-color-input::-webkit-color-swatch-wrapper { 743 + padding: 0; 744 + } 745 + 746 + .custom-color-input::-webkit-color-swatch { 747 + border: 2px solid var(--border); 748 + border-radius: 50%; 749 + } 750 + 751 + .settings-footer { 752 + padding: 1rem; 753 + border-top: 1px solid var(--border); 754 + display: flex; 755 + justify-content: flex-end; 756 + } 757 + 758 + .settings-footer .save-btn { 759 + padding: 0.75rem 1.5rem; 760 + background: var(--accent); 761 + color: white; 762 + border: none; 763 + border-radius: 8px; 764 + cursor: pointer; 765 + font-family: inherit; 766 + font-size: 1rem; 767 + } 768 + 769 + .settings-footer .save-btn:hover { 770 + opacity: 0.9; 771 + } 772 + 773 + .settings-footer .save-btn:disabled { 774 + opacity: 0.5; 775 + cursor: not-allowed; 776 + } 777 + 778 + /* Mobile */ 779 + @media (max-width: 480px) { 780 + .emoji-input-row { 781 + flex-direction: row; 782 + } 783 + 784 + .form-actions { 785 + flex-direction: column; 786 + } 787 + 788 + .emoji-grid { 789 + grid-template-columns: repeat(6, 1fr); 790 + } 791 + }