+5
.gitignore
+5
.gitignore
+33
fly.toml
+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
lexicons.zip
This is a binary file and will not be displayed.
+30
lexicons/preferences.json
+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
+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
+10
site/Caddyfile
+9
site/Dockerfile
+9
site/Dockerfile
+1217
site/app.js
+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
+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
+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
+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
+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
+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
+
}