608 lines
17 KiB
HTML
608 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>GooseFactory — Priority Poker</title>
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--green: #00FF66;
|
|
--yellow: #FFD700;
|
|
--red: #FF3333;
|
|
--bg: #111;
|
|
--card: #1a1a2e;
|
|
--text: #e0e0e0;
|
|
--muted: #888;
|
|
--radius: 14px;
|
|
--felt: #1a6b3c;
|
|
--felt-dark: #0d3d1f;
|
|
}
|
|
|
|
body {
|
|
background: var(--felt-dark);
|
|
color: var(--text);
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Felt texture overlay */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background:
|
|
radial-gradient(ellipse at 50% 30%, rgba(26, 107, 60, 0.4) 0%, transparent 70%),
|
|
repeating-linear-gradient(
|
|
0deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0,0,0,0.03) 2px,
|
|
rgba(0,0,0,0.03) 4px
|
|
),
|
|
repeating-linear-gradient(
|
|
90deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0,0,0,0.02) 2px,
|
|
rgba(0,0,0,0.02) 4px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
/* ── Table Area ── */
|
|
.table-area {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.question-card {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
border-radius: 16px;
|
|
padding: 20px 28px;
|
|
max-width: 480px;
|
|
text-align: center;
|
|
margin-bottom: 32px;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
.question-card .q-label {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1.5px;
|
|
color: rgba(255, 255, 255, 0.4);
|
|
margin-bottom: 8px;
|
|
}
|
|
.question-card h3 {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* ── Played Card Zone ── */
|
|
.played-zone {
|
|
width: 130px;
|
|
height: 180px;
|
|
border: 2px dashed rgba(255, 255, 255, 0.15);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 24px;
|
|
position: relative;
|
|
transition: border-color 0.3s;
|
|
}
|
|
.played-zone.has-card {
|
|
border-color: transparent;
|
|
}
|
|
.played-zone .placeholder-text {
|
|
font-size: 12px;
|
|
color: rgba(255, 255, 255, 0.2);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
/* ── Played Card ── */
|
|
.played-card {
|
|
position: absolute;
|
|
width: 120px;
|
|
height: 170px;
|
|
perspective: 600px;
|
|
}
|
|
.played-card .card-flipper {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
|
transform-style: preserve-3d;
|
|
}
|
|
.played-card.dealing .card-flipper {
|
|
animation: deal-card 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
|
|
}
|
|
|
|
@keyframes deal-card {
|
|
0% { transform: translateY(250px) rotateY(180deg) scale(0.6); opacity: 0; }
|
|
40% { transform: translateY(-20px) rotateY(90deg) scale(1.1); opacity: 1; }
|
|
70% { transform: translateY(5px) rotateY(0deg) scale(1.05); }
|
|
100% { transform: translateY(0) rotateY(0deg) scale(1); }
|
|
}
|
|
|
|
.played-card .pc-face {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 10px;
|
|
backface-visibility: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.played-card .pc-front {
|
|
background: #fff;
|
|
color: #1a1a2e;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.1);
|
|
}
|
|
.played-card .pc-back {
|
|
background: linear-gradient(135deg, #1a237e, #283593);
|
|
transform: rotateY(180deg);
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
|
}
|
|
.played-card .pc-back::after {
|
|
content: '🏭';
|
|
font-size: 36px;
|
|
}
|
|
.played-card .pc-number {
|
|
font-size: 42px;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
}
|
|
.played-card .pc-hint {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-top: 6px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* ── Context Input ── */
|
|
.context-section {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
pointer-events: none;
|
|
margin-bottom: 16px;
|
|
}
|
|
.context-section.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
.context-input {
|
|
width: 100%;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 10px;
|
|
color: #fff;
|
|
font-family: inherit;
|
|
font-size: 14px;
|
|
padding: 12px 16px;
|
|
outline: none;
|
|
backdrop-filter: blur(4px);
|
|
transition: border-color 0.25s;
|
|
}
|
|
.context-input:focus { border-color: rgba(255, 255, 255, 0.35); }
|
|
.context-input::placeholder { color: rgba(255, 255, 255, 0.25); }
|
|
|
|
/* ── Deal Button ── */
|
|
.deal-row {
|
|
width: 100%;
|
|
max-width: 400px;
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) 0.05s;
|
|
pointer-events: none;
|
|
}
|
|
.deal-row.visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
.deal-btn {
|
|
width: 100%;
|
|
padding: 14px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-size: 16px;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
background: linear-gradient(135deg, #cc9900, #aa8800);
|
|
color: #fff;
|
|
transition: all 0.25s;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.deal-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 24px rgba(204, 153, 0, 0.3); }
|
|
.deal-btn:active { transform: scale(0.98); }
|
|
|
|
/* ── Card Fan ── */
|
|
.card-fan {
|
|
position: relative;
|
|
z-index: 2;
|
|
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%);
|
|
padding: 16px 20px 24px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-end;
|
|
gap: 0;
|
|
min-height: 140px;
|
|
}
|
|
|
|
.poker-card {
|
|
width: 70px;
|
|
height: 100px;
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
border: none;
|
|
cursor: pointer;
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
transform-origin: bottom center;
|
|
box-shadow: 2px 4px 12px rgba(0,0,0,0.4);
|
|
user-select: none;
|
|
-webkit-tap-highlight-color: transparent;
|
|
flex-shrink: 0;
|
|
margin: 0 -4px;
|
|
}
|
|
|
|
.poker-card:nth-child(1) { transform: rotate(-16deg) translateY(8px); }
|
|
.poker-card:nth-child(2) { transform: rotate(-12deg) translateY(5px); }
|
|
.poker-card:nth-child(3) { transform: rotate(-8deg) translateY(3px); }
|
|
.poker-card:nth-child(4) { transform: rotate(-4deg) translateY(1px); }
|
|
.poker-card:nth-child(5) { transform: rotate(0deg) translateY(0px); }
|
|
.poker-card:nth-child(6) { transform: rotate(4deg) translateY(1px); }
|
|
.poker-card:nth-child(7) { transform: rotate(8deg) translateY(3px); }
|
|
.poker-card:nth-child(8) { transform: rotate(12deg) translateY(5px); }
|
|
.poker-card:nth-child(9) { transform: rotate(16deg) translateY(8px); }
|
|
|
|
.poker-card:hover {
|
|
transform: translateY(-24px) scale(1.15) !important;
|
|
z-index: 10;
|
|
box-shadow: 0 12px 32px rgba(0,0,0,0.5);
|
|
}
|
|
|
|
.poker-card.selected {
|
|
transform: translateY(-30px) scale(1.15) !important;
|
|
z-index: 10;
|
|
box-shadow: 0 0 24px rgba(255, 215, 0, 0.4), 0 12px 32px rgba(0,0,0,0.5);
|
|
border: 2px solid var(--yellow);
|
|
}
|
|
|
|
.poker-card.dimmed {
|
|
opacity: 0.35;
|
|
transform: scale(0.9) translateY(4px) !important;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.card-number {
|
|
font-size: 24px;
|
|
font-weight: 900;
|
|
color: #1a1a2e;
|
|
line-height: 1;
|
|
}
|
|
.card-label {
|
|
font-size: 8px;
|
|
color: var(--muted);
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-top: 4px;
|
|
}
|
|
.card-suit {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 7px;
|
|
font-size: 10px;
|
|
color: #ccc;
|
|
}
|
|
|
|
/* Corner pips */
|
|
.poker-card::before {
|
|
content: attr(data-pip);
|
|
position: absolute;
|
|
top: 5px;
|
|
left: 7px;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
color: #666;
|
|
}
|
|
|
|
/* ── Done Overlay ── */
|
|
.done-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: var(--felt-dark);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.4s;
|
|
z-index: 100;
|
|
}
|
|
.done-overlay.visible { opacity: 1; pointer-events: auto; }
|
|
.done-overlay .done-emoji { font-size: 64px; margin-bottom: 12px; }
|
|
.done-overlay .done-text { font-size: 18px; color: var(--muted); }
|
|
|
|
/* ── Thwack animation ── */
|
|
@keyframes thwack {
|
|
0% { transform: scale(1.15); }
|
|
50% { transform: scale(0.95); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
.played-zone.thwack {
|
|
animation: thwack 0.25s ease-out;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="table-area">
|
|
<div class="question-card">
|
|
<div class="q-label">Estimate</div>
|
|
<h3 id="questionText">Loading…</h3>
|
|
</div>
|
|
|
|
<div class="played-zone" id="playedZone">
|
|
<span class="placeholder-text">Play a card</span>
|
|
</div>
|
|
|
|
<div class="context-section" id="contextSection">
|
|
<input type="text" class="context-input" id="contextInput" placeholder="Any context for your estimate? (optional)" maxlength="200">
|
|
</div>
|
|
|
|
<div class="deal-row" id="dealRow">
|
|
<button class="deal-btn" id="dealBtn">🃏 Deal</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-fan" id="cardFan">
|
|
<button class="poker-card" data-value="1" data-pip="1">
|
|
<span class="card-suit">♠</span>
|
|
<span class="card-number">1</span>
|
|
<span class="card-label">Trivial</span>
|
|
</button>
|
|
<button class="poker-card" data-value="2" data-pip="2">
|
|
<span class="card-suit">♥</span>
|
|
<span class="card-number">2</span>
|
|
<span class="card-label">Simple</span>
|
|
</button>
|
|
<button class="poker-card" data-value="3" data-pip="3">
|
|
<span class="card-suit">♦</span>
|
|
<span class="card-number">3</span>
|
|
<span class="card-label">Easy</span>
|
|
</button>
|
|
<button class="poker-card" data-value="5" data-pip="5">
|
|
<span class="card-suit">♣</span>
|
|
<span class="card-number">5</span>
|
|
<span class="card-label">Medium</span>
|
|
</button>
|
|
<button class="poker-card" data-value="8" data-pip="8">
|
|
<span class="card-suit">♠</span>
|
|
<span class="card-number">8</span>
|
|
<span class="card-label">Hard</span>
|
|
</button>
|
|
<button class="poker-card" data-value="13" data-pip="13">
|
|
<span class="card-suit">♥</span>
|
|
<span class="card-number">13</span>
|
|
<span class="card-label">V. Hard</span>
|
|
</button>
|
|
<button class="poker-card" data-value="21" data-pip="21">
|
|
<span class="card-suit">♦</span>
|
|
<span class="card-number">21</span>
|
|
<span class="card-label">Enormous</span>
|
|
</button>
|
|
<button class="poker-card" data-value="?" data-pip="?">
|
|
<span class="card-suit">🔮</span>
|
|
<span class="card-number">?</span>
|
|
<span class="card-label">No idea</span>
|
|
</button>
|
|
<button class="poker-card" data-value="break" data-pip="☕">
|
|
<span class="card-suit">☕</span>
|
|
<span class="card-number">☕</span>
|
|
<span class="card-label">Break</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="done-overlay" id="doneOverlay">
|
|
<div class="done-emoji">🃏</div>
|
|
<div class="done-text">Estimate dealt</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
const ctx = window.__FACTORY_CONTEXT__ || {};
|
|
const modalType = 'priority-poker';
|
|
const modalVersion = '1.0.0';
|
|
const startTime = Date.now();
|
|
|
|
function post(msg) { try { window.parent.postMessage(msg, '*'); } catch(e) {} }
|
|
post({ type: 'factory_modal_ready', modalType, version: modalVersion });
|
|
|
|
// ── Populate question ──
|
|
document.getElementById('questionText').textContent =
|
|
ctx.question || ctx.itemName || 'Estimate the effort for this task';
|
|
|
|
// ── State ──
|
|
let selectedValue = null;
|
|
let interactionCount = 0;
|
|
let firstInteractionTime = null;
|
|
let hoveredCards = [];
|
|
let selectionTime = null;
|
|
|
|
function trackInteraction() {
|
|
interactionCount++;
|
|
if (!firstInteractionTime) firstInteractionTime = Date.now();
|
|
}
|
|
|
|
// ── Card selection ──
|
|
const cards = document.querySelectorAll('.poker-card');
|
|
const playedZone = document.getElementById('playedZone');
|
|
const contextSection = document.getElementById('contextSection');
|
|
const dealRow = document.getElementById('dealRow');
|
|
|
|
// Track hovers
|
|
cards.forEach(card => {
|
|
card.addEventListener('mouseenter', () => {
|
|
const val = card.dataset.value;
|
|
if (!hoveredCards.includes(val)) hoveredCards.push(val);
|
|
});
|
|
});
|
|
|
|
cards.forEach(card => {
|
|
card.addEventListener('click', () => {
|
|
trackInteraction();
|
|
selectedValue = card.dataset.value;
|
|
selectionTime = Date.now();
|
|
|
|
// Visual state
|
|
cards.forEach(c => {
|
|
c.classList.remove('selected', 'dimmed');
|
|
if (c !== card) c.classList.add('dimmed');
|
|
});
|
|
card.classList.add('selected');
|
|
|
|
// Show played card on table
|
|
const number = card.querySelector('.card-number').textContent;
|
|
const hint = card.querySelector('.card-label').textContent;
|
|
|
|
playedZone.innerHTML = '';
|
|
playedZone.classList.add('has-card');
|
|
|
|
const playedCard = document.createElement('div');
|
|
playedCard.className = 'played-card dealing';
|
|
playedCard.innerHTML = `
|
|
<div class="card-flipper">
|
|
<div class="pc-face pc-front">
|
|
<div class="pc-number">${number}</div>
|
|
<div class="pc-hint">${hint}</div>
|
|
</div>
|
|
<div class="pc-face pc-back"></div>
|
|
</div>
|
|
`;
|
|
playedZone.appendChild(playedCard);
|
|
|
|
// Thwack effect
|
|
setTimeout(() => {
|
|
playedZone.classList.add('thwack');
|
|
setTimeout(() => playedZone.classList.remove('thwack'), 300);
|
|
}, 500);
|
|
|
|
// Show context + deal
|
|
contextSection.classList.add('visible');
|
|
dealRow.classList.add('visible');
|
|
});
|
|
});
|
|
|
|
// ── Allow re-selection ──
|
|
// If you click a dimmed card, un-dim all and re-select
|
|
cards.forEach(card => {
|
|
card.addEventListener('click', () => {
|
|
// This handler runs after the one above since both are on same element
|
|
// The above handler already handles re-selection correctly
|
|
});
|
|
});
|
|
|
|
// ── Deal button ──
|
|
document.getElementById('dealBtn').addEventListener('click', () => {
|
|
if (!selectedValue) return;
|
|
trackInteraction();
|
|
|
|
const responseTimeMs = Date.now() - startTime;
|
|
const contextText = document.getElementById('contextInput').value.trim();
|
|
|
|
// Map value
|
|
let estimate;
|
|
if (selectedValue === '?') estimate = 'unknown';
|
|
else if (selectedValue === 'break') estimate = 'break';
|
|
else estimate = parseInt(selectedValue);
|
|
|
|
const hintMap = {
|
|
'1': 'Trivial', '2': 'Simple', '3': 'Easy', '5': 'Medium',
|
|
'8': 'Hard', '13': 'Very Hard', '21': 'Enormous', '?': 'Unknown', 'break': 'Need a Break'
|
|
};
|
|
|
|
const payload = {
|
|
type: 'factory_modal_response',
|
|
modalType,
|
|
modalVersion,
|
|
pipelineId: ctx.pipelineId || 'unknown',
|
|
itemId: ctx.itemId || 'unknown',
|
|
sessionId: ctx.sessionId || 'sess_' + Date.now(),
|
|
timestamp: new Date().toISOString(),
|
|
responseTimeMs,
|
|
feedback: {
|
|
decision: selectedValue === 'break' ? 'break' : 'estimated',
|
|
estimate,
|
|
estimate_label: hintMap[selectedValue] || selectedValue,
|
|
context: contextText || undefined,
|
|
tags: contextText ? ['has_context'] : [],
|
|
},
|
|
meta: {
|
|
timeToFirstInteractionMs: firstInteractionTime ? firstInteractionTime - startTime : null,
|
|
timeToDecisionMs: selectionTime ? selectionTime - startTime : responseTimeMs,
|
|
selectionTimeMs: selectionTime ? selectionTime - startTime : null,
|
|
totalInteractions: interactionCount,
|
|
hoveredCards,
|
|
estimation_range: hoveredCards.length > 1 ? {
|
|
lowest: hoveredCards[0],
|
|
highest: hoveredCards[hoveredCards.length - 1],
|
|
considered: hoveredCards.length,
|
|
} : null,
|
|
deviceType: window.innerWidth < 768 ? 'mobile' : 'desktop',
|
|
viewportSize: { width: window.innerWidth, height: window.innerHeight },
|
|
responseTimeMs,
|
|
},
|
|
};
|
|
|
|
post(payload);
|
|
|
|
// Done state
|
|
document.getElementById('doneOverlay').classList.add('visible');
|
|
setTimeout(() => {
|
|
post({ type: 'factory_modal_close', reason: 'completed' });
|
|
}, 1200);
|
|
});
|
|
|
|
// ── Keyboard shortcuts ──
|
|
document.addEventListener('keydown', (e) => {
|
|
const keyMap = { '1': '1', '2': '2', '3': '3', '5': '5', '8': '8' };
|
|
if (keyMap[e.key]) {
|
|
const card = document.querySelector(`.poker-card[data-value="${keyMap[e.key]}"]`);
|
|
if (card) card.click();
|
|
}
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|