feat: require vote confirmation

This commit is contained in:
TopherMayor
2026-04-30 11:28:33 -07:00
parent 8726eb7cf9
commit 4ad51ed2c6

View File

@@ -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 @@
</div>
</div>
<!-- Vote Confirmation Modal -->
<div class="modal-overlay hidden" id="voteConfirmModal">
<div class="modal">
<div class="drag-handle"></div>
<h2 id="voteConfirmTitle">Confirm Vote</h2>
<p id="voteConfirmText">Are you sure you want to vote for this option?</p>
<div class="modal-actions">
<button type="button" class="btn-secondary" onclick="closeVoteConfirm()">Cancel</button>
<button type="button" id="voteConfirmActionBtn" onclick="confirmVote()">Vote</button>
</div>
</div>
</div>
<!-- Header -->
<header>
<h1>📍 Cabo Bachelor Party — Vote</h1>
@@ -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 ? `<a href="${opt.url}" target="_blank" rel="noopener noreferrer" class="option-link" onclick="event.stopPropagation()">🔗 ${opt.url.replace(/^https?:\/\//, '').split('/')[0]}</a>` : '');
return `
<div class="option-card${hasVoted ? ' voted' : ''}" onclick="toggleVote('${opt.id}')">
<div class="option-card${hasVoted ? ' voted' : ''}">
<div class="option-top">
<div class="option-name">${opt.name}</div>
<div class="option-votes">${voteEntries.length} vote${voteEntries.length !== 1 ? 's' : ''}</div>
@@ -2183,6 +2295,16 @@
${linkPills}
${renderOptionFacts(opt)}
${renderPriceTrend(opt)}
<div class="option-actions">
<button
type="button"
class="option-vote-btn${hasVoted ? ' voted' : ''}"
title="Vote"
onclick="openVoteConfirm('${opt.id}', ${hasVoted ? 'true' : 'false'}); event.stopPropagation()"
>
Vote
</button>
</div>
<div class="vote-bar-bg">
<div class="vote-bar-fill ${catClass}" style="width:${votePct}%"></div>
</div>
@@ -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 ────────────────────────────────────────────