Update
This commit is contained in:
@@ -529,6 +529,10 @@ func (m *Manager) marshalRoomState(room *Room, viewerParticipantID string) ([]by
|
|||||||
|
|
||||||
participants := make([]PublicParticipant, 0, len(room.Participants))
|
participants := make([]PublicParticipant, 0, len(room.Participants))
|
||||||
for _, participant := range sortParticipants(room.Participants) {
|
for _, participant := range sortParticipants(room.Participants) {
|
||||||
|
if !participant.Connected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
public := PublicParticipant{
|
public := PublicParticipant{
|
||||||
ID: participant.ID,
|
ID: participant.ID,
|
||||||
Username: participant.Username,
|
Username: participant.Username,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/main.css">
|
<link rel="stylesheet" href="/static/css/main.css">
|
||||||
<link rel="stylesheet" href="/static/css/layout.css">
|
<link rel="stylesheet" href="/static/css/layout.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/cards.css">
|
||||||
<link rel="stylesheet" href="/static/css/themes/win98.css">
|
<link rel="stylesheet" href="/static/css/themes/win98.css">
|
||||||
<link rel="stylesheet" href="/static/css/themes/modern.css">
|
<link rel="stylesheet" href="/static/css/themes/modern.css">
|
||||||
<link rel="stylesheet" href="/static/css/themes/no-theme.css">
|
<link rel="stylesheet" href="/static/css/themes/no-theme.css">
|
||||||
@@ -195,6 +196,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/static/js/ui-controls.js"></script>
|
<script src="/static/js/ui-controls.js"></script>
|
||||||
|
<script src="/static/js/cards.js"></script>
|
||||||
<script src="/static/js/config.js"></script>
|
<script src="/static/js/config.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/main.css">
|
<link rel="stylesheet" href="/static/css/main.css">
|
||||||
<link rel="stylesheet" href="/static/css/layout.css">
|
<link rel="stylesheet" href="/static/css/layout.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/cards.css">
|
||||||
<link rel="stylesheet" href="/static/css/themes/win98.css">
|
<link rel="stylesheet" href="/static/css/themes/win98.css">
|
||||||
<link rel="stylesheet" href="/static/css/themes/modern.css">
|
<link rel="stylesheet" href="/static/css/themes/modern.css">
|
||||||
<link rel="stylesheet" href="/static/css/themes/no-theme.css">
|
<link rel="stylesheet" href="/static/css/themes/no-theme.css">
|
||||||
@@ -161,6 +162,7 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/static/js/ui-controls.js"></script>
|
<script src="/static/js/ui-controls.js"></script>
|
||||||
|
<script src="/static/js/cards.js"></script>
|
||||||
<script src="/static/js/room.js"></script>
|
<script src="/static/js/room.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
38
static/css/cards.css
Normal file
38
static/css/cards.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.vote-card,
|
||||||
|
.preview-card {
|
||||||
|
border: var(--card-border-width) solid var(--card-border);
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--card-text);
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-corner {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-corner.top-left {
|
||||||
|
top: 0.34rem;
|
||||||
|
left: 0.34rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-corner.bottom-right {
|
||||||
|
right: 0.34rem;
|
||||||
|
bottom: 0.34rem;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-center-icon {
|
||||||
|
z-index: 1;
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -110,10 +110,6 @@
|
|||||||
width: 3.15rem;
|
width: 3.15rem;
|
||||||
height: 4.45rem;
|
height: 4.45rem;
|
||||||
border-radius: 0.32rem;
|
border-radius: 0.32rem;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
transition: transform 170ms ease;
|
transition: transform 170ms ease;
|
||||||
@@ -290,10 +286,6 @@
|
|||||||
width: 4.3rem;
|
width: 4.3rem;
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
border-radius: 0.4rem;
|
border-radius: 0.4rem;
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 120ms ease;
|
transition: transform 120ms ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,41 +158,6 @@ input[type="number"]::-webkit-inner-spin-button {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vote-card,
|
|
||||||
.preview-card {
|
|
||||||
border: var(--card-border-width) solid var(--card-border);
|
|
||||||
background: var(--card-bg);
|
|
||||||
color: var(--card-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-corner {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
line-height: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-corner.top-left {
|
|
||||||
top: 0.34rem;
|
|
||||||
left: 0.34rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-corner.bottom-right {
|
|
||||||
right: 0.34rem;
|
|
||||||
bottom: 0.34rem;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-center-icon {
|
|
||||||
z-index: 1;
|
|
||||||
line-height: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vote-card.is-selected {
|
.vote-card.is-selected {
|
||||||
outline: var(--selected-outline);
|
outline: var(--selected-outline);
|
||||||
outline-offset: -0.35rem;
|
outline-offset: -0.35rem;
|
||||||
|
|||||||
41
static/js/cards.js
Normal file
41
static/js/cards.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
(() => {
|
||||||
|
const CARD_ICONS = ['★', '◆', '✦', '☀', '☘', '⚙', '♣', '♠', '♥', '♦', '✚', '⚡', '☾', '✿'];
|
||||||
|
|
||||||
|
function iconForValue(value) {
|
||||||
|
const normalized = String(value || '');
|
||||||
|
if (normalized === '?') return '❓';
|
||||||
|
if (normalized === '☕') return '☕';
|
||||||
|
if (normalized === '∞') return '∞';
|
||||||
|
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < normalized.length; i += 1) {
|
||||||
|
hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0;
|
||||||
|
}
|
||||||
|
return CARD_ICONS[hash % CARD_ICONS.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendFace(el, value) {
|
||||||
|
const normalized = String(value || '');
|
||||||
|
|
||||||
|
const topLeft = document.createElement('span');
|
||||||
|
topLeft.className = 'card-corner top-left';
|
||||||
|
topLeft.textContent = normalized;
|
||||||
|
|
||||||
|
const center = document.createElement('span');
|
||||||
|
center.className = 'card-center-icon';
|
||||||
|
center.textContent = iconForValue(normalized);
|
||||||
|
|
||||||
|
const bottomRight = document.createElement('span');
|
||||||
|
bottomRight.className = 'card-corner bottom-right';
|
||||||
|
bottomRight.textContent = normalized;
|
||||||
|
|
||||||
|
el.appendChild(topLeft);
|
||||||
|
el.appendChild(center);
|
||||||
|
el.appendChild(bottomRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.CardUI = {
|
||||||
|
iconForValue,
|
||||||
|
appendFace,
|
||||||
|
};
|
||||||
|
})();
|
||||||
@@ -13,8 +13,6 @@ const SPECIAL_CARD_ORDER = {
|
|||||||
'☕': 3,
|
'☕': 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CARD_ICONS = ['★', '◆', '✦', '☀', '☘', '⚙', '♣', '♠', '♥', '♦', '✚', '⚡', '☾', '✿'];
|
|
||||||
|
|
||||||
const roomConfigForm = document.getElementById('room-config-form');
|
const roomConfigForm = document.getElementById('room-config-form');
|
||||||
const statusLine = document.getElementById('config-status');
|
const statusLine = document.getElementById('config-status');
|
||||||
const scaleSelect = document.getElementById('estimation-scale');
|
const scaleSelect = document.getElementById('estimation-scale');
|
||||||
@@ -49,6 +47,10 @@ if (savedUsername && !usernameInput.value) {
|
|||||||
usernameInput.value = savedUsername;
|
usernameInput.value = savedUsername;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!window.CardUI || typeof window.CardUI.appendFace !== 'function') {
|
||||||
|
throw new Error('CardUI is not loaded. Ensure /static/js/cards.js is included before config.js.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function parseNumericCard(value) {
|
function parseNumericCard(value) {
|
||||||
if (!/^-?\d+(\.\d+)?$/.test(value)) {
|
if (!/^-?\d+(\.\d+)?$/.test(value)) {
|
||||||
@@ -84,36 +86,6 @@ function createCard(value) {
|
|||||||
return { id: String(nextCardID++), value: value.toString() };
|
return { id: String(nextCardID++), value: value.toString() };
|
||||||
}
|
}
|
||||||
|
|
||||||
function iconForCard(value) {
|
|
||||||
if (value === '?') return '❓';
|
|
||||||
if (value === '☕') return '☕';
|
|
||||||
if (value === '∞') return '∞';
|
|
||||||
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < value.length; i += 1) {
|
|
||||||
hash = (hash * 31 + value.charCodeAt(i)) >>> 0;
|
|
||||||
}
|
|
||||||
return CARD_ICONS[hash % CARD_ICONS.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCardFace(el, value) {
|
|
||||||
const topLeft = document.createElement('span');
|
|
||||||
topLeft.className = 'card-corner top-left';
|
|
||||||
topLeft.textContent = value;
|
|
||||||
|
|
||||||
const center = document.createElement('span');
|
|
||||||
center.className = 'card-center-icon';
|
|
||||||
center.textContent = iconForCard(value);
|
|
||||||
|
|
||||||
const bottomRight = document.createElement('span');
|
|
||||||
bottomRight.className = 'card-corner bottom-right';
|
|
||||||
bottomRight.textContent = value;
|
|
||||||
|
|
||||||
el.appendChild(topLeft);
|
|
||||||
el.appendChild(center);
|
|
||||||
el.appendChild(bottomRight);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCardsForScale(scale) {
|
function getCardsForScale(scale) {
|
||||||
return (SCALE_PRESETS[scale] || SCALE_PRESETS.fibonacci).map(createCard);
|
return (SCALE_PRESETS[scale] || SCALE_PRESETS.fibonacci).map(createCard);
|
||||||
}
|
}
|
||||||
@@ -190,7 +162,7 @@ function buildCardElement(card) {
|
|||||||
cardEl.className = 'preview-card';
|
cardEl.className = 'preview-card';
|
||||||
cardEl.dataset.cardId = card.id;
|
cardEl.dataset.cardId = card.id;
|
||||||
cardEl.draggable = true;
|
cardEl.draggable = true;
|
||||||
appendCardFace(cardEl, card.value);
|
window.CardUI.appendFace(cardEl, card.value);
|
||||||
|
|
||||||
const removeBtn = document.createElement('button');
|
const removeBtn = document.createElement('button');
|
||||||
removeBtn.type = 'button';
|
removeBtn.type = 'button';
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const USERNAME_KEY = 'scrumPoker.username';
|
const USERNAME_KEY = 'scrumPoker.username';
|
||||||
const CARD_ICONS = ['★', '◆', '✦', '☀', '☘', '⚙', '♣', '♠', '♥', '♦', '✚', '⚡', '☾', '✿'];
|
|
||||||
|
|
||||||
const roomID = document.body.dataset.roomId;
|
const roomID = document.body.dataset.roomId;
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -37,6 +36,10 @@ const savedUsername = localStorage.getItem(USERNAME_KEY) || '';
|
|||||||
joinUsernameInput.value = savedUsername;
|
joinUsernameInput.value = savedUsername;
|
||||||
joinAdminTokenInput.value = adminToken;
|
joinAdminTokenInput.value = adminToken;
|
||||||
|
|
||||||
|
if (!window.CardUI || typeof window.CardUI.appendFace !== 'function') {
|
||||||
|
throw new Error('CardUI is not loaded. Ensure /static/js/cards.js is included before room.js.');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function setJoinError(message) {
|
function setJoinError(message) {
|
||||||
if (!message) {
|
if (!message) {
|
||||||
@@ -100,7 +103,8 @@ async function joinRoom({ username, role, password, participantIdOverride }) {
|
|||||||
function renderParticipants(participants, isRevealed) {
|
function renderParticipants(participants, isRevealed) {
|
||||||
participantList.innerHTML = '';
|
participantList.innerHTML = '';
|
||||||
|
|
||||||
participants.forEach((participant) => {
|
const visibleParticipants = participants.filter((participant) => participant.connected);
|
||||||
|
visibleParticipants.forEach((participant) => {
|
||||||
const item = document.createElement('li');
|
const item = document.createElement('li');
|
||||||
item.className = 'participant-item';
|
item.className = 'participant-item';
|
||||||
|
|
||||||
@@ -138,41 +142,15 @@ function parseNumericVote(value) {
|
|||||||
return Number(value);
|
return Number(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function iconForCard(value) {
|
|
||||||
if (value === '?') return '❓';
|
|
||||||
if (value === '☕') return '☕';
|
|
||||||
if (value === '∞') return '∞';
|
|
||||||
|
|
||||||
let hash = 0;
|
|
||||||
for (let i = 0; i < value.length; i += 1) {
|
|
||||||
hash = (hash * 31 + value.charCodeAt(i)) >>> 0;
|
|
||||||
}
|
|
||||||
return CARD_ICONS[hash % CARD_ICONS.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCardFace(el, value) {
|
|
||||||
const topLeft = document.createElement('span');
|
|
||||||
topLeft.className = 'card-corner top-left';
|
|
||||||
topLeft.textContent = value;
|
|
||||||
|
|
||||||
const center = document.createElement('span');
|
|
||||||
center.className = 'card-center-icon';
|
|
||||||
center.textContent = iconForCard(value);
|
|
||||||
|
|
||||||
const bottomRight = document.createElement('span');
|
|
||||||
bottomRight.className = 'card-corner bottom-right';
|
|
||||||
bottomRight.textContent = value;
|
|
||||||
|
|
||||||
el.appendChild(topLeft);
|
|
||||||
el.appendChild(center);
|
|
||||||
el.appendChild(bottomRight);
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateSummary(state) {
|
function calculateSummary(state) {
|
||||||
const rows = new Map();
|
const rows = new Map();
|
||||||
const numericVotes = [];
|
const numericVotes = [];
|
||||||
|
|
||||||
state.participants.forEach((participant) => {
|
state.participants.forEach((participant) => {
|
||||||
|
if (!participant.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (participant.role !== 'participant' || !participant.hasVoted || !participant.voteValue) {
|
if (participant.role !== 'participant' || !participant.hasVoted || !participant.voteValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -242,7 +220,7 @@ function renderSummary(state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCards(cards, participants, isRevealed) {
|
function renderCards(cards, participants, isRevealed) {
|
||||||
const self = participants.find((participant) => participant.id === participantID);
|
const self = participants.find((participant) => participant.id === participantID && participant.connected);
|
||||||
const canVote = self && self.role === 'participant';
|
const canVote = self && self.role === 'participant';
|
||||||
const selfVote = self ? self.voteValue : '';
|
const selfVote = self ? self.voteValue : '';
|
||||||
|
|
||||||
@@ -253,7 +231,7 @@ function renderCards(cards, participants, isRevealed) {
|
|||||||
card.type = 'button';
|
card.type = 'button';
|
||||||
card.className = 'vote-card';
|
card.className = 'vote-card';
|
||||||
card.setAttribute('aria-label', `Vote ${value}`);
|
card.setAttribute('aria-label', `Vote ${value}`);
|
||||||
appendCardFace(card, value);
|
window.CardUI.appendFace(card, value);
|
||||||
|
|
||||||
if (selfVote === value && !isRevealed) {
|
if (selfVote === value && !isRevealed) {
|
||||||
card.classList.add('is-selected');
|
card.classList.add('is-selected');
|
||||||
@@ -289,8 +267,8 @@ function renderState(state) {
|
|||||||
adminControls.classList.add('hidden');
|
adminControls.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
const votedCount = state.participants.filter((p) => p.role === 'participant' && p.hasVoted).length;
|
const votedCount = state.participants.filter((p) => p.connected && p.role === 'participant' && p.hasVoted).length;
|
||||||
const totalParticipants = state.participants.filter((p) => p.role === 'participant').length;
|
const totalParticipants = state.participants.filter((p) => p.connected && p.role === 'participant').length;
|
||||||
roomStatus.textContent = `Votes: ${votedCount}/${totalParticipants}`;
|
roomStatus.textContent = `Votes: ${votedCount}/${totalParticipants}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user