Compare commits
2 Commits
791af99662
...
e4e555cac3
| Author | SHA1 | Date | |
|---|---|---|---|
| e4e555cac3 | |||
| 2b63370873 |
@@ -86,7 +86,20 @@
|
|||||||
|
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<label for="room-password">Room password (optional)</label>
|
<label for="room-password">Room password (optional)</label>
|
||||||
<input type="password" id="room-password" name="password" maxlength="64" placeholder="Optional password">
|
<input
|
||||||
|
type="password"
|
||||||
|
id="room-password"
|
||||||
|
name="password"
|
||||||
|
maxlength="64"
|
||||||
|
placeholder="Optional password"
|
||||||
|
autocomplete="new-password"
|
||||||
|
data-bwignore="true"
|
||||||
|
data-1p-ignore="true"
|
||||||
|
data-lpignore="true"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -362,6 +362,17 @@ body.is-dragging-window .ui-tool-title-bar {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-panel-window {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-panel-window > .window-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.participants-window {
|
.participants-window {
|
||||||
min-height: 24rem;
|
min-height: 24rem;
|
||||||
}
|
}
|
||||||
@@ -421,12 +432,13 @@ body.is-dragging-window .ui-tool-title-bar {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
height: 100%;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.participants-scroll {
|
.participants-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 13rem;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,9 @@ function parseNumericVote(value) {
|
|||||||
|
|
||||||
function calculateSummary(state) {
|
function calculateSummary(state) {
|
||||||
const rows = new Map();
|
const rows = new Map();
|
||||||
const numericVotes = [];
|
const scoreVotes = [];
|
||||||
|
const cardIndexByValue = new Map(state.cards.map((cardValue, index) => [cardValue, index]));
|
||||||
|
let hasNumericVote = false;
|
||||||
|
|
||||||
state.participants.forEach((participant) => {
|
state.participants.forEach((participant) => {
|
||||||
if (!participant.connected) {
|
if (!participant.connected) {
|
||||||
@@ -221,26 +223,42 @@ function calculateSummary(state) {
|
|||||||
|
|
||||||
const numeric = parseNumericVote(participant.voteValue);
|
const numeric = parseNumericVote(participant.voteValue);
|
||||||
if (numeric !== null) {
|
if (numeric !== null) {
|
||||||
numericVotes.push(numeric);
|
hasNumericVote = true;
|
||||||
|
scoreVotes.push(numeric);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cardIndexByValue.has(participant.voteValue)) {
|
||||||
|
scoreVotes.push(cardIndexByValue.get(participant.voteValue));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let average = null;
|
let average = null;
|
||||||
if (numericVotes.length > 0) {
|
if (scoreVotes.length > 0) {
|
||||||
average = numericVotes.reduce((acc, value) => acc + value, 0) / numericVotes.length;
|
average = scoreVotes.reduce((acc, value) => acc + value, 0) / scoreVotes.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deckNumeric = state.cards
|
let averageCard = null;
|
||||||
.map(parseNumericVote)
|
|
||||||
.filter((value) => value !== null)
|
|
||||||
.sort((a, b) => a - b);
|
|
||||||
|
|
||||||
let recommended = null;
|
let recommended = null;
|
||||||
if (average !== null && deckNumeric.length > 0) {
|
if (!hasNumericVote && average !== null && state.cards.length > 0) {
|
||||||
recommended = deckNumeric.find((value) => value >= average) ?? deckNumeric[deckNumeric.length - 1];
|
const roundedIndex = Math.min(
|
||||||
|
state.cards.length - 1,
|
||||||
|
Math.max(0, Math.round(average)),
|
||||||
|
);
|
||||||
|
averageCard = state.cards[roundedIndex];
|
||||||
|
recommended = state.cards[roundedIndex];
|
||||||
|
} else {
|
||||||
|
const deckNumeric = state.cards
|
||||||
|
.map(parseNumericVote)
|
||||||
|
.filter((value) => value !== null)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
if (average !== null && deckNumeric.length > 0) {
|
||||||
|
recommended = deckNumeric.find((value) => value >= average) ?? deckNumeric[deckNumeric.length - 1];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { rows, average, recommended };
|
return { rows, average, recommended, averageCard };
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSummary(state) {
|
function renderSummary(state) {
|
||||||
@@ -251,7 +269,7 @@ function renderSummary(state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rows, average, recommended } = calculateSummary(state);
|
const { rows, average, recommended, averageCard } = calculateSummary(state);
|
||||||
summaryBody.innerHTML = '';
|
summaryBody.innerHTML = '';
|
||||||
|
|
||||||
if (rows.size === 0) {
|
if (rows.size === 0) {
|
||||||
@@ -274,7 +292,11 @@ function renderSummary(state) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
summaryAverage.textContent = average === null ? 'Average: -' : `Average: ${average.toFixed(2)}`;
|
if (averageCard !== null) {
|
||||||
|
summaryAverage.textContent = `Average: ${averageCard}`;
|
||||||
|
} else {
|
||||||
|
summaryAverage.textContent = average === null ? 'Average: -' : `Average: ${average.toFixed(2)}`;
|
||||||
|
}
|
||||||
summaryRecommended.textContent = recommended === null ? 'Recommended: -' : `Recommended: ${recommended}`;
|
summaryRecommended.textContent = recommended === null ? 'Recommended: -' : `Recommended: ${recommended}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,6 +422,34 @@ function updateShareLink() {
|
|||||||
shareLinkInput.value = raw ? `${window.location.origin}${raw}` : '';
|
shareLinkInput.value = raw ? `${window.location.origin}${raw}` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fallbackCopyFromShareInput() {
|
||||||
|
shareLinkInput.focus();
|
||||||
|
shareLinkInput.select();
|
||||||
|
shareLinkInput.setSelectionRange(0, shareLinkInput.value.length);
|
||||||
|
document.execCommand('copy');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectAndCopyShareLink() {
|
||||||
|
if (!shareLinkInput.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shareLinkInput.focus();
|
||||||
|
shareLinkInput.select();
|
||||||
|
shareLinkInput.setSelectionRange(0, shareLinkInput.value.length);
|
||||||
|
|
||||||
|
if (!navigator.clipboard || !window.isSecureContext) {
|
||||||
|
fallbackCopyFromShareInput();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareLinkInput.value);
|
||||||
|
} catch (_err) {
|
||||||
|
fallbackCopyFromShareInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function connectSSE() {
|
function connectSSE() {
|
||||||
if (eventSource) {
|
if (eventSource) {
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
@@ -502,6 +552,9 @@ async function changeName() {
|
|||||||
revealBtn.addEventListener('click', () => adminAction('reveal'));
|
revealBtn.addEventListener('click', () => adminAction('reveal'));
|
||||||
resetBtn.addEventListener('click', () => adminAction('reset'));
|
resetBtn.addEventListener('click', () => adminAction('reset'));
|
||||||
shareAdminToggle.addEventListener('change', updateShareLink);
|
shareAdminToggle.addEventListener('change', updateShareLink);
|
||||||
|
shareLinkInput.addEventListener('click', () => {
|
||||||
|
void selectAndCopyShareLink();
|
||||||
|
});
|
||||||
changeNameBtn.addEventListener('click', () => {
|
changeNameBtn.addEventListener('click', () => {
|
||||||
void changeName();
|
void changeName();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,10 @@
|
|||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasOwn(obj, key) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||||
|
}
|
||||||
|
|
||||||
function parseJSON(value, fallback) {
|
function parseJSON(value, fallback) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -153,7 +157,58 @@
|
|||||||
saveWindowLayouts();
|
saveWindowLayouts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureWindowLayout(def) {
|
function measureLayoutFromContent(def) {
|
||||||
|
const windowEl = def.el;
|
||||||
|
const fallback = def.defaultLayout;
|
||||||
|
const wasHidden = windowEl.classList.contains('hidden');
|
||||||
|
const previousStyles = {
|
||||||
|
visibility: windowEl.style.visibility,
|
||||||
|
left: windowEl.style.left,
|
||||||
|
top: windowEl.style.top,
|
||||||
|
width: windowEl.style.width,
|
||||||
|
height: windowEl.style.height,
|
||||||
|
right: windowEl.style.right,
|
||||||
|
bottom: windowEl.style.bottom,
|
||||||
|
transform: windowEl.style.transform,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (wasHidden) {
|
||||||
|
windowEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
windowEl.style.visibility = 'hidden';
|
||||||
|
windowEl.style.left = '-10000px';
|
||||||
|
windowEl.style.top = '-10000px';
|
||||||
|
windowEl.style.width = 'max-content';
|
||||||
|
windowEl.style.height = 'auto';
|
||||||
|
windowEl.style.right = 'auto';
|
||||||
|
windowEl.style.bottom = 'auto';
|
||||||
|
windowEl.style.transform = 'none';
|
||||||
|
|
||||||
|
const rect = windowEl.getBoundingClientRect();
|
||||||
|
|
||||||
|
windowEl.style.visibility = previousStyles.visibility;
|
||||||
|
windowEl.style.left = previousStyles.left;
|
||||||
|
windowEl.style.top = previousStyles.top;
|
||||||
|
windowEl.style.width = previousStyles.width;
|
||||||
|
windowEl.style.height = previousStyles.height;
|
||||||
|
windowEl.style.right = previousStyles.right;
|
||||||
|
windowEl.style.bottom = previousStyles.bottom;
|
||||||
|
windowEl.style.transform = previousStyles.transform;
|
||||||
|
|
||||||
|
if (wasHidden) {
|
||||||
|
windowEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeLayout({
|
||||||
|
left: fallback.left,
|
||||||
|
top: fallback.top,
|
||||||
|
width: Number.isFinite(rect.width) && rect.width > 0 ? Math.ceil(rect.width) : fallback.width,
|
||||||
|
height: Number.isFinite(rect.height) && rect.height > 0 ? Math.ceil(rect.height) : fallback.height,
|
||||||
|
}, fallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureWindowLayout(def, options = {}) {
|
||||||
if (isMobileViewport()) {
|
if (isMobileViewport()) {
|
||||||
def.el.style.right = 'auto';
|
def.el.style.right = 'auto';
|
||||||
def.el.style.bottom = 'auto';
|
def.el.style.bottom = 'auto';
|
||||||
@@ -161,7 +216,12 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = normalizeLayout(windowLayouts[def.id], def.defaultLayout);
|
const hasSavedLayout = hasOwn(options, 'hasSavedLayout')
|
||||||
|
? options.hasSavedLayout
|
||||||
|
: hasOwn(windowLayouts, def.id);
|
||||||
|
const normalized = hasSavedLayout
|
||||||
|
? normalizeLayout(windowLayouts[def.id], def.defaultLayout)
|
||||||
|
: measureLayoutFromContent(def);
|
||||||
const clamped = clampLayoutToViewport(normalized);
|
const clamped = clampLayoutToViewport(normalized);
|
||||||
applyLayout(def.el, clamped);
|
applyLayout(def.el, clamped);
|
||||||
windowLayouts[def.id] = clamped;
|
windowLayouts[def.id] = clamped;
|
||||||
@@ -284,11 +344,20 @@
|
|||||||
|
|
||||||
function initWindowLayouts() {
|
function initWindowLayouts() {
|
||||||
windowLayouts = loadWindowLayouts();
|
windowLayouts = loadWindowLayouts();
|
||||||
|
let shouldPersistSeededLayouts = false;
|
||||||
|
|
||||||
windowDefs.forEach((def) => {
|
windowDefs.forEach((def) => {
|
||||||
ensureWindowLayout(def);
|
const hasSavedLayout = hasOwn(windowLayouts, def.id);
|
||||||
|
ensureWindowLayout(def, { hasSavedLayout });
|
||||||
|
if (!hasSavedLayout) {
|
||||||
|
shouldPersistSeededLayouts = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (shouldPersistSeededLayouts) {
|
||||||
|
saveWindowLayouts();
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (isMobileViewport()) {
|
if (isMobileViewport()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user