Something went wrong. Try again.
Voting service on AT Protocol.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233import { attachTypeahead } from './components/HandleTypeahead.js';import { bskyPostUrl } from './api.js';import { resultsGrid, RESULTS_CSS } from './results.js';import { FOOTER_HTML, FOOTER_CSS } from './footer.js';import { esc } from './utils.js';import { createElement, ArrowRight, Plus, User } from 'lucide';
// the shared results + footer components ship their own css (also used// verbatim by the worker's public pages)const resultsStyle = document.createElement('style');resultsStyle.textContent = RESULTS_CSS + FOOTER_CSS;document.head.appendChild(resultsStyle);// footer lives inside main-screen so it only shows once signed indocument.getElementById('main-screen').insertAdjacentHTML('beforeend', FOOTER_HTML);
const ARROW_RIGHT_HTML = createElement(ArrowRight, { width: 16, height: 16, 'stroke-width': 2, class: 'inline align-text-bottom ml-1' }).outerHTML;const PLUS_HTML = createElement(Plus, { width: 14, height: 14, 'stroke-width': 2, class: 'shrink-0' }).outerHTML;
const OPTION_INPUT_CLASS = 'option-input block w-full text-base p-1.5 bg-stone-100 dark:bg-stone-800 outline-none mt-2 resize-none overflow-hidden';const MAX_OPTIONS = 12;
export function showLoading(msg) { document.getElementById('login-screen').style.display = 'none'; document.getElementById('main-screen').style.display = 'none'; const ls = document.getElementById('loading-screen'); ls.style.display = 'block'; ls.textContent = msg || 'loading…';}
export function showMain() { document.getElementById('login-screen').style.display = 'none'; document.getElementById('loading-screen').style.display = 'none'; document.getElementById('main-screen').style.display = 'block';}
export function setWhoami(profile) { const el = document.getElementById('whoami'); if (!el) return; el.title = `@${profile.handle}`; el.className = 'flex items-center gap-2 min-w-0 text-gray-600 dark:text-gray-400';
const avatar = profile.avatar ? `<img src="${esc(profile.avatar)}" alt="@${esc(profile.handle)}" class="w-8 h-8 object-cover block shrink-0" />` : `<span class="avatar-slot w-8 h-8 flex items-center justify-center shrink-0"></span>`; el.innerHTML = avatar + `<span class="truncate text-black dark:text-gray-300">${esc(profile.displayName || '@' + profile.handle)}</span>`;
const slot = el.querySelector('.avatar-slot'); if (slot) slot.appendChild(createElement(User, { width: 16, height: 16, 'stroke-width': 2 }));}
export function showLoginError(msg) { document.getElementById('login-error').textContent = msg || '';}
export function showError(msg) { document.getElementById('error').textContent = msg || '';}
export function setLoginLoading(isLoading) { const btn = document.getElementById('login-btn'); btn.disabled = isLoading; btn.innerHTML = isLoading ? 'redirecting…' : `sign in${ARROW_RIGHT_HTML}`; if (isLoading) showLoginError('');}
export function setPublishLoading(isLoading) { const btn = document.getElementById('create-poll-btn'); btn.disabled = isLoading; btn.textContent = isLoading ? 'publishing…' : 'publish poll';}
// bluesky-composer-style budget donut: fills as you type, shows the// remaining count when it gets close, goes red with the overage when overexport function updatePostBudget(graphemes, max) { const el = document.getElementById('post-budget'); if (!el) return; const remaining = max - graphemes; const over = remaining < 0; const r = 7; const c = 2 * Math.PI * r; const frac = Math.min(graphemes / max, 1); el.className = `flex items-center gap-1.5 tabular-nums text-sm ${over ? 'text-red-500' : 'text-gray-600 dark:text-gray-400'}`; el.innerHTML = (over || remaining <= 20 ? `<span>${remaining}</span>` : '') + `<svg width="18" height="18" viewBox="0 0 18 18" class="-rotate-90 shrink-0"> <circle cx="9" cy="9" r="${r}" fill="none" stroke="currentColor" stroke-opacity="0.25" stroke-width="2"></circle> <circle cx="9" cy="9" r="${r}" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="${c}" stroke-dashoffset="${c * (1 - frac)}" stroke-linecap="butt"></circle> </svg>`;
const btn = document.getElementById('create-poll-btn'); btn.disabled = over; btn.classList.toggle('opacity-40', over);}
export function setPollsDimmed(dimmed) { const classList = document.getElementById('polls').classList; if (dimmed) { classList.add('opacity-40', 'pointer-events-none'); } else { classList.remove('opacity-40', 'pointer-events-none'); }}
export function readPollForm() { const question = document.getElementById('poll-question').value.trim(); const options = [...document.querySelectorAll('.option-input')] .map(i => i.value.trim()) .filter(Boolean); return { question, options };}
export function resetPollForm() { const q = document.getElementById('poll-question'); q.value = ''; autoGrow(q); const container = document.getElementById('option-inputs'); container.innerHTML = ''; addOptionInput(); addOptionInput();}
function autoGrow(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px';}
function addOptionInput() { const container = document.getElementById('option-inputs'); if (container.children.length >= MAX_OPTIONS) return; const input = document.createElement('textarea'); input.rows = 1; input.className = OPTION_INPUT_CLASS; input.placeholder = `option ${container.children.length + 1}`; input.autocomplete = 'off'; // options are single-line (they wrap visually, but hold no newlines — // each becomes one facet line in the post); the question allows breaks input.addEventListener('keydown', e => { if (e.key === 'Enter') e.preventDefault(); }); input.addEventListener('input', () => { if (input.value.includes('\n')) input.value = input.value.replace(/\s*\n\s*/g, ' '); autoGrow(input); }); container.appendChild(input); document.getElementById('add-option-btn').style.display = container.children.length >= MAX_OPTIONS ? 'none' : '';}
// polls: [{ uri, question, createdAt, options: [{ uri, cid, text, count }] }]export function renderPolls(polls, onDelete) { const el = document.getElementById('polls'); el.innerHTML = '';
if (!polls.length) { el.innerHTML = ''; return; }
polls.forEach(poll => { const total = poll.options.reduce((sum, o) => sum + o.count, 0); const card = document.createElement('div'); card.className = 'mb-12 break-words';
card.innerHTML = `<div class="q">${esc(poll.question)}</div>` + `<div class="flex items-baseline justify-between gap-2 mb-1.5">` + `<div class="vote-count text-gray-600 dark:text-gray-400 tabular-nums">${total.toLocaleString()} votes</div>` + `<span class="shrink-0 inline-flex gap-4 text-gray-600 dark:text-gray-400">` + (poll.postUri ? `<a href="${esc(bskyPostUrl(poll.postUri))}" target="_blank" rel="noopener" class="italic underline underline-offset-2 hover:text-black dark:hover:text-gray-300">view</a>` : '') + `<span class="delete-btn cursor-pointer italic underline underline-offset-2 hover:text-black dark:hover:text-gray-300" data-poll="${esc(poll.uri)}">delete</span>` + `</span>` + `</div>` + resultsGrid(poll.options, total);
if (poll.votedFor) { const mine = card.querySelector(`.opt-text[data-option="${CSS.escape(poll.votedFor)}"]`); if (mine) mine.textContent += ' ←'; }
el.appendChild(card); });
// destructive, so two-step: [delete] arms into really?, second click fires el.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', () => { if (btn.dataset.armed) { onDelete(btn.dataset.poll); } else { btn.dataset.armed = '1'; btn.textContent = 'really?'; btn.classList.add('text-red-500'); } }); btn.addEventListener('mouseleave', () => { delete btn.dataset.armed; btn.textContent = 'delete'; btn.classList.remove('text-red-500'); }); });}
export function bindEvents({ onLogin, onLogout, onCreatePoll, onPollInput }) { setLoginLoading(false); const handleInput = document.getElementById('handle'); document.getElementById('login-btn').addEventListener('click', () => { onLogin(handleInput.value.trim()); });
attachTypeahead(handleInput, document.getElementById('handle-typeahead'), { onSubmit: v => onLogin(v), });
document.getElementById('logout-btn').addEventListener('click', onLogout);
const question = document.getElementById('poll-question'); question.addEventListener('input', () => autoGrow(question));
const addBtn = document.getElementById('add-option-btn'); addBtn.innerHTML = `${PLUS_HTML} add option`; addBtn.addEventListener('click', addOptionInput); addOptionInput(); addOptionInput();
document.getElementById('create-poll-btn').addEventListener('click', () => { onCreatePoll(readPollForm()); });
if (onPollInput) { document.getElementById('poll-form').addEventListener('input', onPollInput); onPollInput(); }}