feat: require vote confirmation
This commit is contained in:
@@ -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 ────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user