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>