From 4ad51ed2c64f6f772f3e1bd7c489b8ff863d4e5e Mon Sep 17 00:00:00 2001 From: TopherMayor Date: Thu, 30 Apr 2026 11:28:33 -0700 Subject: [PATCH] feat: require vote confirmation --- public/index.html | 177 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 141 insertions(+), 36 deletions(-) diff --git a/public/index.html b/public/index.html index 9000c44..eb61934 100644 --- a/public/index.html +++ b/public/index.html @@ -462,6 +462,25 @@ transition: opacity 0.2s; } .modal button:hover { opacity: 0.85; } + .modal-actions { + display: flex; + gap: 10px; + margin-top: 8px; + } + .modal-actions button { + width: auto; + flex: 1; + } + .modal-actions .btn-secondary { + background: transparent; + color: var(--text); + border: 1px solid var(--border); + } + .modal-actions .btn-secondary:hover { + border-color: var(--accent); + color: var(--accent); + opacity: 1; + } /* ── Tabs ───────────────────────────────────────────────── */ .tabs { @@ -544,7 +563,7 @@ border-radius: 12px; padding: 14px 16px; transition: border-color 0.2s, transform 0.15s; - cursor: pointer; + cursor: default; position: relative; overflow: hidden; } @@ -562,6 +581,40 @@ font-size: 0.85rem; font-weight: 700; } + .option-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + margin: 10px 0 2px; + flex-wrap: wrap; + } + .option-vote-btn { + border: 1px solid rgba(0,212,255,0.32); + background: rgba(0,212,255,0.10); + color: #dff9ff; + border-radius: 999px; + padding: 7px 12px; + font-size: 0.74rem; + font-weight: 800; + letter-spacing: 0.2px; + cursor: pointer; + transition: transform 0.15s, border-color 0.2s, background 0.2s; + } + .option-vote-btn:hover { + border-color: rgba(0,212,255,0.55); + background: rgba(0,212,255,0.16); + transform: translateY(-1px); + } + .option-vote-btn.voted { + border-color: rgba(52,211,153,0.35); + background: rgba(52,211,153,0.12); + color: #d8fff0; + } + .option-vote-btn.voted:hover { + border-color: rgba(52,211,153,0.6); + background: rgba(52,211,153,0.18); + } .option-top { display: flex; @@ -1199,22 +1252,7 @@ .option-card { min-height: 72px; padding: 14px 16px; } .option-name { font-size: 0.92rem; } .option-desc { font-size: 0.76rem; } - - /* Add arrow hint to cards */ - .option-card::after { - content: '›'; - position: absolute; - right: 14px; - top: 50%; - transform: translateY(-50%); - color: var(--text-muted); - font-size: 1.2rem; - opacity: 0.4; - } - .option-card.voted::after { display: none; } - - /* Touch feedback */ - .option-card:active { transform: scale(0.98); opacity: 0.9; } + .option-actions { justify-content: flex-start; } } /* ── Desktop (≥641px) ──────────────────────────────────── */ @@ -1293,6 +1331,19 @@ + + +

📍 Cabo Bachelor Party — Vote

@@ -1425,6 +1476,8 @@ let ws = null; let activeTab = 'hotel'; let priceRefreshTimer = null; + let pendingVoteOptionId = null; + let pendingVoteRemove = false; // ── Init ─────────────────────────────────────────────────── function init() { @@ -2011,6 +2064,9 @@ applyVoterName(name); document.getElementById('nameModal').classList.add('hidden'); render(); + if (pendingVoteOptionId) { + openVoteConfirm(pendingVoteOptionId, pendingVoteRemove); + } } function applyVoterName(name) { @@ -2024,6 +2080,62 @@ document.getElementById('voterNameInput').focus(); } + function openVoteConfirm(optionId, remove = false) { + if (activeTab === 'results') return; + if (!state.voterName) { + pendingVoteOptionId = optionId; + pendingVoteRemove = remove; + document.getElementById('nameModal').classList.remove('hidden'); + document.getElementById('voterNameInput').focus(); + return; + } + if (!state.pollsOpen) { + showToast('Polls are currently closed', 'error'); + return; + } + + const opt = state.options.find(o => o.id === optionId); + if (!opt) return; + + const alreadyVoted = getVoteEntries(opt).some(v => v.name === state.voterName); + pendingVoteOptionId = optionId; + pendingVoteRemove = remove || alreadyVoted; + + const confirmTitle = document.getElementById('voteConfirmTitle'); + const confirmText = document.getElementById('voteConfirmText'); + const confirmBtn = document.getElementById('voteConfirmActionBtn'); + const isRemoveAction = pendingVoteRemove; + + if (confirmTitle) confirmTitle.textContent = isRemoveAction ? 'Remove Vote' : 'Confirm Vote'; + if (confirmText) { + confirmText.textContent = isRemoveAction + ? `You already voted for ${opt.name}. Confirm to remove your vote.` + : `Confirm your vote for ${opt.name}.`; + } + if (confirmBtn) confirmBtn.textContent = isRemoveAction ? 'Remove Vote' : 'Vote'; + document.getElementById('voteConfirmModal').classList.remove('hidden'); + } + + function closeVoteConfirm() { + pendingVoteOptionId = null; + pendingVoteRemove = false; + document.getElementById('voteConfirmModal').classList.add('hidden'); + } + + function confirmVote() { + if (!pendingVoteOptionId) return; + const optionId = pendingVoteOptionId; + const remove = pendingVoteRemove; + const opt = state.options.find(o => o.id === optionId); + if (!opt) { + closeVoteConfirm(); + return; + } + wsSend({ type: 'vote', optionId, voterName: state.voterName, remove }); + showToast(remove ? `Removed vote for ${opt.name}` : `Voted for ${opt.name}!`); + closeVoteConfirm(); + } + // ── Tabs ─────────────────────────────────────────────────── function renderTabs() { const bar = document.getElementById('tabsBar'); @@ -2174,7 +2286,7 @@ : (opt.url ? `🔗 ${opt.url.replace(/^https?:\/\//, '').split('/')[0]}` : ''); return ` -
+
${opt.name}
${voteEntries.length} vote${voteEntries.length !== 1 ? 's' : ''}
@@ -2183,6 +2295,16 @@ ${linkPills} ${renderOptionFacts(opt)} ${renderPriceTrend(opt)} +
+ +
@@ -2227,24 +2349,7 @@ // ── Voting ──────────────────────────────────────────────── function toggleVote(optionId) { - if (activeTab === 'results') return; // no voting on results tab - if (!state.voterName) { - document.getElementById('nameModal').classList.remove('hidden'); - document.getElementById('voterNameInput').focus(); - return; - } - if (!state.pollsOpen) { - showToast('Polls are currently closed', 'error'); - return; - } - - const opt = state.options.find(o => o.id === optionId); - if (!opt) return; - - const alreadyVoted = getVoteEntries(opt).some(v => v.name === state.voterName); - - wsSend({ type: 'vote', optionId, voterName: state.voterName, remove: alreadyVoted }); - showToast(alreadyVoted ? `Removed vote for ${opt.name}` : `Voted for ${opt.name}!`); + openVoteConfirm(optionId); } // ── Add option ────────────────────────────────────────────