Updateees
This commit is contained in:
@@ -19,7 +19,9 @@ const resetBtn = document.getElementById('reset-btn');
|
||||
const terminalBtn = document.getElementById('terminal-btn');
|
||||
const shareLinkInput = document.getElementById('share-link');
|
||||
const shareAdminToggle = document.getElementById('share-admin-toggle');
|
||||
const roomStatus = document.getElementById('room-status');
|
||||
const votesCounter = document.getElementById('votes-counter');
|
||||
const roomMessage = document.getElementById('room-message');
|
||||
const changeNameBtn = document.getElementById('change-name-btn');
|
||||
const terminalModalOverlay = document.getElementById('terminal-modal-overlay');
|
||||
const terminalCloseBtn = document.getElementById('terminal-close-btn');
|
||||
const terminalLogOutput = document.getElementById('terminal-log-output');
|
||||
@@ -37,6 +39,7 @@ const prefillUsername = params.get('username') || '';
|
||||
let eventSource = null;
|
||||
let latestLinks = { participantLink: '', adminLink: '' };
|
||||
let latestAdminLogs = [];
|
||||
let latestRole = joinRoleInput.value || 'participant';
|
||||
|
||||
const savedUsername = localStorage.getItem(USERNAME_KEY) || '';
|
||||
joinUsernameInput.value = prefillUsername || savedUsername;
|
||||
@@ -84,6 +87,10 @@ function activateRoomView() {
|
||||
joinPanel.classList.add('hidden');
|
||||
}
|
||||
|
||||
function setRoomMessage(message) {
|
||||
roomMessage.textContent = message;
|
||||
}
|
||||
|
||||
async function joinRoom({ username, role, password, participantIdOverride }) {
|
||||
const response = await fetch(`/api/rooms/${encodeURIComponent(roomID)}/join`, {
|
||||
method: 'POST',
|
||||
@@ -326,6 +333,11 @@ function renderState(state) {
|
||||
renderCards(state.cards, state.participants, state.revealed);
|
||||
renderSummary(state);
|
||||
|
||||
const self = state.participants.find((participant) => participant.id === participantID && participant.connected);
|
||||
if (self && self.role) {
|
||||
latestRole = self.role;
|
||||
}
|
||||
|
||||
latestLinks = state.links || { participantLink: '', adminLink: '' };
|
||||
updateShareLink();
|
||||
|
||||
@@ -344,7 +356,7 @@ function renderState(state) {
|
||||
|
||||
const votedCount = state.participants.filter((p) => p.connected && p.role === 'participant' && p.hasVoted).length;
|
||||
const totalParticipants = state.participants.filter((p) => p.connected && p.role === 'participant').length;
|
||||
roomStatus.textContent = `Votes: ${votedCount}/${totalParticipants}`;
|
||||
votesCounter.textContent = `Votes: ${votedCount}/${totalParticipants}`;
|
||||
}
|
||||
|
||||
function updateShareLink() {
|
||||
@@ -364,13 +376,14 @@ function connectSSE() {
|
||||
const payload = JSON.parse(event.data);
|
||||
renderState(payload);
|
||||
activateRoomView();
|
||||
setRoomMessage('Connected.');
|
||||
} catch (_err) {
|
||||
roomStatus.textContent = 'Failed to parse room update.';
|
||||
setRoomMessage('Failed to parse room update.');
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onerror = () => {
|
||||
roomStatus.textContent = 'Connection interrupted. Retrying...';
|
||||
setRoomMessage('Connection interrupted. Retrying...');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -388,10 +401,10 @@ async function castVote(card) {
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
roomStatus.textContent = data.error || 'Vote rejected.';
|
||||
setRoomMessage(data.error || 'Vote rejected.');
|
||||
}
|
||||
} catch (_err) {
|
||||
roomStatus.textContent = 'Network error while casting vote.';
|
||||
setRoomMessage('Network error while casting vote.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,10 +422,45 @@ async function adminAction(action) {
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
roomStatus.textContent = data.error || `Unable to ${action}.`;
|
||||
setRoomMessage(data.error || `Unable to ${action}.`);
|
||||
}
|
||||
} catch (_err) {
|
||||
roomStatus.textContent = 'Network error while sending admin action.';
|
||||
setRoomMessage('Network error while sending admin action.');
|
||||
}
|
||||
}
|
||||
|
||||
async function changeName() {
|
||||
if (!participantID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = joinUsernameInput.value.trim() || localStorage.getItem(USERNAME_KEY) || '';
|
||||
const next = window.prompt('Enter your new name:', current);
|
||||
if (next === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = next.trim();
|
||||
if (!username) {
|
||||
setRoomMessage('Name cannot be empty.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (username === current) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await joinRoom({
|
||||
username,
|
||||
role: latestRole || joinRoleInput.value || 'participant',
|
||||
password: joinPasswordInput.value,
|
||||
participantIdOverride: participantID,
|
||||
});
|
||||
joinUsernameInput.value = result.username;
|
||||
setRoomMessage('Name updated.');
|
||||
} catch (err) {
|
||||
setRoomMessage(err.message || 'Unable to change name.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +474,9 @@ terminalModalOverlay.addEventListener('click', (event) => {
|
||||
}
|
||||
});
|
||||
shareAdminToggle.addEventListener('change', updateShareLink);
|
||||
changeNameBtn.addEventListener('click', () => {
|
||||
void changeName();
|
||||
});
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closeTerminal();
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
(() => {
|
||||
const THEME_KEY = 'scrumPoker.ui.theme';
|
||||
const MODE_KEY = 'scrumPoker.ui.mode';
|
||||
const WINDOW_LAYOUTS_KEY = 'scrumPoker.ui.windowLayouts.v1';
|
||||
const DEFAULT_THEME = 'win98';
|
||||
const MODE_ICON_LIGHT = '/static/img/Windows Icons - PNG/desk.cpl_14_40-0.png';
|
||||
const MODE_ICON_DARK = '/static/img/Windows Icons - PNG/desk.cpl_14_40-6.png';
|
||||
const DEFAULT_WINDOW_LAYOUTS = {
|
||||
'theme-tool-window': { left: 16, top: 88, width: 390, height: 250 },
|
||||
'mode-tool-window': { left: 424, top: 88, width: 340, height: 190 },
|
||||
};
|
||||
let floatingWindowZ = 80;
|
||||
let windowLayouts = {};
|
||||
|
||||
function applyTheme(theme) {
|
||||
const normalized = theme || DEFAULT_THEME;
|
||||
@@ -20,16 +29,273 @@
|
||||
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function isWindowOpen(id) {
|
||||
const win = document.getElementById(id);
|
||||
return Boolean(win && !win.classList.contains('hidden'));
|
||||
}
|
||||
|
||||
function syncTaskButtons() {
|
||||
document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
|
||||
const target = button.dataset.target;
|
||||
button.classList.toggle('is-active', isWindowOpen(target));
|
||||
});
|
||||
}
|
||||
|
||||
function syncControls() {
|
||||
const theme = document.documentElement.getAttribute('data-ui-theme') || DEFAULT_THEME;
|
||||
const mode = getCurrentMode();
|
||||
const modeLabel = mode === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode';
|
||||
const modeIcon = mode === 'dark' ? MODE_ICON_DARK : MODE_ICON_LIGHT;
|
||||
|
||||
document.querySelectorAll('[data-role="theme-picker"]').forEach((el) => {
|
||||
el.value = theme;
|
||||
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
|
||||
const selected = button.dataset.theme === theme;
|
||||
button.classList.toggle('is-selected', selected);
|
||||
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-role="mode-toggle"]').forEach((el) => {
|
||||
el.textContent = mode === 'dark' ? 'Light Mode' : 'Dark Mode';
|
||||
document.querySelectorAll('[data-role="mode-toggle-label"]').forEach((el) => {
|
||||
el.textContent = modeLabel;
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-role="mode-icon"]').forEach((el) => {
|
||||
el.src = modeIcon;
|
||||
});
|
||||
|
||||
document.querySelectorAll('#mode-status-text').forEach((el) => {
|
||||
el.textContent = `Current mode: ${mode === 'dark' ? 'Dark' : 'Light'}`;
|
||||
});
|
||||
|
||||
syncTaskButtons();
|
||||
}
|
||||
|
||||
function bringWindowToFront(windowEl) {
|
||||
floatingWindowZ += 1;
|
||||
windowEl.style.zIndex = String(floatingWindowZ);
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function loadWindowLayouts() {
|
||||
try {
|
||||
const raw = localStorage.getItem(WINDOW_LAYOUTS_KEY);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return {};
|
||||
}
|
||||
return parsed;
|
||||
} catch (_err) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveWindowLayouts() {
|
||||
localStorage.setItem(WINDOW_LAYOUTS_KEY, JSON.stringify(windowLayouts));
|
||||
}
|
||||
|
||||
function normalizeLayout(raw, defaults) {
|
||||
const fallback = defaults || { left: 16, top: 88, width: 360, height: 220 };
|
||||
const minWidth = 260;
|
||||
const minHeight = 160;
|
||||
|
||||
const width = Number(raw?.width);
|
||||
const height = Number(raw?.height);
|
||||
const left = Number(raw?.left);
|
||||
const top = Number(raw?.top);
|
||||
|
||||
return {
|
||||
width: Number.isFinite(width) ? Math.max(minWidth, width) : fallback.width,
|
||||
height: Number.isFinite(height) ? Math.max(minHeight, height) : fallback.height,
|
||||
left: Number.isFinite(left) ? left : fallback.left,
|
||||
top: Number.isFinite(top) ? top : fallback.top,
|
||||
};
|
||||
}
|
||||
|
||||
function clampLayoutToViewport(layout) {
|
||||
const margin = 8;
|
||||
const minWidth = 260;
|
||||
const minHeight = 160;
|
||||
const maxWidth = Math.max(minWidth, window.innerWidth - margin * 2);
|
||||
const maxHeight = Math.max(minHeight, window.innerHeight - margin * 2);
|
||||
|
||||
const width = clamp(layout.width, minWidth, maxWidth);
|
||||
const height = clamp(layout.height, minHeight, maxHeight);
|
||||
const left = clamp(layout.left, margin, Math.max(margin, window.innerWidth - width - margin));
|
||||
const top = clamp(layout.top, margin, Math.max(margin, window.innerHeight - height - margin));
|
||||
return { left, top, width, height };
|
||||
}
|
||||
|
||||
function applyLayout(windowEl, layout) {
|
||||
windowEl.style.left = `${layout.left}px`;
|
||||
windowEl.style.top = `${layout.top}px`;
|
||||
windowEl.style.width = `${layout.width}px`;
|
||||
windowEl.style.height = `${layout.height}px`;
|
||||
windowEl.style.right = 'auto';
|
||||
windowEl.style.bottom = 'auto';
|
||||
windowEl.style.transform = 'none';
|
||||
}
|
||||
|
||||
function readLayoutFromDOM(windowEl) {
|
||||
const rect = windowEl.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
function persistWindowLayout(windowEl) {
|
||||
const id = windowEl.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const next = clampLayoutToViewport(readLayoutFromDOM(windowEl));
|
||||
windowLayouts[id] = next;
|
||||
saveWindowLayouts();
|
||||
}
|
||||
|
||||
function ensureWindowLayout(windowEl) {
|
||||
const id = windowEl.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const defaults = DEFAULT_WINDOW_LAYOUTS[id];
|
||||
const saved = windowLayouts[id];
|
||||
const normalized = normalizeLayout(saved, defaults);
|
||||
const clamped = clampLayoutToViewport(normalized);
|
||||
applyLayout(windowEl, clamped);
|
||||
windowLayouts[id] = clamped;
|
||||
}
|
||||
|
||||
function openToolWindow(id) {
|
||||
const windowEl = document.getElementById(id);
|
||||
if (!windowEl) {
|
||||
return;
|
||||
}
|
||||
ensureWindowLayout(windowEl);
|
||||
windowEl.classList.remove('hidden');
|
||||
bringWindowToFront(windowEl);
|
||||
syncTaskButtons();
|
||||
}
|
||||
|
||||
function closeToolWindow(id) {
|
||||
const windowEl = document.getElementById(id);
|
||||
if (!windowEl) {
|
||||
return;
|
||||
}
|
||||
windowEl.classList.add('hidden');
|
||||
syncTaskButtons();
|
||||
}
|
||||
|
||||
function initDraggableWindows() {
|
||||
let dragState = null;
|
||||
|
||||
document.querySelectorAll('[data-role="drag-handle"]').forEach((handle) => {
|
||||
handle.addEventListener('pointerdown', (event) => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
if (event.target.closest('button')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowEl = handle.closest('.ui-tool-window');
|
||||
if (!windowEl || windowEl.classList.contains('hidden')) {
|
||||
return;
|
||||
}
|
||||
|
||||
bringWindowToFront(windowEl);
|
||||
const rect = windowEl.getBoundingClientRect();
|
||||
windowEl.style.left = `${rect.left}px`;
|
||||
windowEl.style.top = `${rect.top}px`;
|
||||
windowEl.style.right = 'auto';
|
||||
windowEl.style.bottom = 'auto';
|
||||
windowEl.style.transform = 'none';
|
||||
|
||||
dragState = {
|
||||
pointerId: event.pointerId,
|
||||
windowEl,
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top,
|
||||
};
|
||||
|
||||
document.body.classList.add('is-dragging-window');
|
||||
handle.setPointerCapture(event.pointerId);
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('pointermove', (event) => {
|
||||
if (!dragState || dragState.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const windowEl = dragState.windowEl;
|
||||
const maxLeft = Math.max(0, window.innerWidth - windowEl.offsetWidth);
|
||||
const maxTop = Math.max(0, window.innerHeight - windowEl.offsetHeight);
|
||||
const nextLeft = clamp(event.clientX - dragState.offsetX, 0, maxLeft);
|
||||
const nextTop = clamp(event.clientY - dragState.offsetY, 0, maxTop);
|
||||
|
||||
windowEl.style.left = `${nextLeft}px`;
|
||||
windowEl.style.top = `${nextTop}px`;
|
||||
});
|
||||
|
||||
function finishDrag(event) {
|
||||
if (!dragState || dragState.pointerId !== event.pointerId) {
|
||||
return;
|
||||
}
|
||||
persistWindowLayout(dragState.windowEl);
|
||||
dragState = null;
|
||||
document.body.classList.remove('is-dragging-window');
|
||||
}
|
||||
|
||||
window.addEventListener('pointerup', finishDrag);
|
||||
window.addEventListener('pointercancel', finishDrag);
|
||||
}
|
||||
|
||||
function initResizableWindows() {
|
||||
if (typeof ResizeObserver !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const windowEl = entry.target;
|
||||
if (windowEl.classList.contains('hidden')) {
|
||||
return;
|
||||
}
|
||||
persistWindowLayout(windowEl);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
||||
observer.observe(windowEl);
|
||||
});
|
||||
}
|
||||
|
||||
function initWindowLayouts() {
|
||||
windowLayouts = loadWindowLayouts();
|
||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
||||
ensureWindowLayout(windowEl);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
||||
const id = windowEl.id;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeLayout(windowLayouts[id], DEFAULT_WINDOW_LAYOUTS[id]);
|
||||
const clamped = clampLayoutToViewport(normalized);
|
||||
applyLayout(windowEl, clamped);
|
||||
windowLayouts[id] = clamped;
|
||||
});
|
||||
saveWindowLayouts();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,18 +310,19 @@
|
||||
const savedMode = localStorage.getItem(MODE_KEY) || 'light';
|
||||
applyTheme(savedTheme);
|
||||
applyMode(savedMode);
|
||||
initWindowLayouts();
|
||||
syncControls();
|
||||
|
||||
document.querySelectorAll('[data-role="theme-picker"]').forEach((picker) => {
|
||||
picker.addEventListener('change', (event) => {
|
||||
const value = event.target.value || DEFAULT_THEME;
|
||||
document.querySelectorAll('[data-role="theme-option"]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const value = button.dataset.theme || DEFAULT_THEME;
|
||||
localStorage.setItem(THEME_KEY, value);
|
||||
applyTheme(value);
|
||||
syncControls();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-role="mode-toggle"]').forEach((button) => {
|
||||
document.querySelectorAll('[data-role="mode-toggle-action"]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const next = getCurrentMode() === 'dark' ? 'light' : 'dark';
|
||||
localStorage.setItem(MODE_KEY, next);
|
||||
@@ -63,6 +330,55 @@
|
||||
syncControls();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-role="open-window"]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const target = button.dataset.target;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (isWindowOpen(target)) {
|
||||
closeToolWindow(target);
|
||||
return;
|
||||
}
|
||||
openToolWindow(target);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-role="close-window"]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const target = button.dataset.target;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
closeToolWindow(target);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
||||
windowEl.addEventListener('pointerdown', () => {
|
||||
if (windowEl.classList.contains('hidden')) {
|
||||
return;
|
||||
}
|
||||
bringWindowToFront(windowEl);
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
document.querySelectorAll('.ui-tool-window').forEach((windowEl) => {
|
||||
if (windowEl.classList.contains('hidden')) {
|
||||
return;
|
||||
}
|
||||
windowEl.classList.add('hidden');
|
||||
});
|
||||
syncTaskButtons();
|
||||
});
|
||||
|
||||
initDraggableWindows();
|
||||
initResizableWindows();
|
||||
}
|
||||
|
||||
window.initUIControls = initUIControls;
|
||||
|
||||
Reference in New Issue
Block a user