feat: require vote confirmation
This commit is contained in:
@@ -462,6 +462,25 @@
|
|||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
.modal button:hover { opacity: 0.85; }
|
.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 ───────────────────────────────────────────────── */
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -544,7 +563,7 @@
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
transition: border-color 0.2s, transform 0.15s;
|
transition: border-color 0.2s, transform 0.15s;
|
||||||
cursor: pointer;
|
cursor: default;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -562,6 +581,40 @@
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 700;
|
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 {
|
.option-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1199,22 +1252,7 @@
|
|||||||
.option-card { min-height: 72px; padding: 14px 16px; }
|
.option-card { min-height: 72px; padding: 14px 16px; }
|
||||||
.option-name { font-size: 0.92rem; }
|
.option-name { font-size: 0.92rem; }
|
||||||
.option-desc { font-size: 0.76rem; }
|
.option-desc { font-size: 0.76rem; }
|
||||||
|
.option-actions { justify-content: flex-start; }
|
||||||
/* 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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Desktop (≥641px) ──────────────────────────────────── */
|
/* ── Desktop (≥641px) ──────────────────────────────────── */
|
||||||
@@ -1293,6 +1331,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</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 -->
|
||||||
<header>
|
<header>
|
||||||
<h1>📍 Cabo Bachelor Party — Vote</h1>
|
<h1>📍 Cabo Bachelor Party — Vote</h1>
|
||||||
@@ -1425,6 +1476,8 @@
|
|||||||
let ws = null;
|
let ws = null;
|
||||||
let activeTab = 'hotel';
|
let activeTab = 'hotel';
|
||||||
let priceRefreshTimer = null;
|
let priceRefreshTimer = null;
|
||||||
|
let pendingVoteOptionId = null;
|
||||||
|
let pendingVoteRemove = false;
|
||||||
|
|
||||||
// ── Init ───────────────────────────────────────────────────
|
// ── Init ───────────────────────────────────────────────────
|
||||||
function init() {
|
function init() {
|
||||||
@@ -2011,6 +2064,9 @@
|
|||||||
applyVoterName(name);
|
applyVoterName(name);
|
||||||
document.getElementById('nameModal').classList.add('hidden');
|
document.getElementById('nameModal').classList.add('hidden');
|
||||||
render();
|
render();
|
||||||
|
if (pendingVoteOptionId) {
|
||||||
|
openVoteConfirm(pendingVoteOptionId, pendingVoteRemove);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyVoterName(name) {
|
function applyVoterName(name) {
|
||||||
@@ -2024,6 +2080,62 @@
|
|||||||
document.getElementById('voterNameInput').focus();
|
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 ───────────────────────────────────────────────────
|
// ── Tabs ───────────────────────────────────────────────────
|
||||||
function renderTabs() {
|
function renderTabs() {
|
||||||
const bar = document.getElementById('tabsBar');
|
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>` : '');
|
: (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 `
|
return `
|
||||||
<div class="option-card${hasVoted ? ' voted' : ''}" onclick="toggleVote('${opt.id}')">
|
<div class="option-card${hasVoted ? ' voted' : ''}">
|
||||||
<div class="option-top">
|
<div class="option-top">
|
||||||
<div class="option-name">${opt.name}</div>
|
<div class="option-name">${opt.name}</div>
|
||||||
<div class="option-votes">${voteEntries.length} vote${voteEntries.length !== 1 ? 's' : ''}</div>
|
<div class="option-votes">${voteEntries.length} vote${voteEntries.length !== 1 ? 's' : ''}</div>
|
||||||
@@ -2183,6 +2295,16 @@
|
|||||||
${linkPills}
|
${linkPills}
|
||||||
${renderOptionFacts(opt)}
|
${renderOptionFacts(opt)}
|
||||||
${renderPriceTrend(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-bg">
|
||||||
<div class="vote-bar-fill ${catClass}" style="width:${votePct}%"></div>
|
<div class="vote-bar-fill ${catClass}" style="width:${votePct}%"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2227,24 +2349,7 @@
|
|||||||
|
|
||||||
// ── Voting ────────────────────────────────────────────────
|
// ── Voting ────────────────────────────────────────────────
|
||||||
function toggleVote(optionId) {
|
function toggleVote(optionId) {
|
||||||
if (activeTab === 'results') return; // no voting on results tab
|
openVoteConfirm(optionId);
|
||||||
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}!`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Add option ────────────────────────────────────────────
|
// ── Add option ────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user