Files
scrum-solitare/frontend-mockup.html
2026-03-05 21:25:59 +02:00

520 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Retro Scrum Poker</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<style>
/* --- THEME VARIABLES --- */
:root {
/* Light Theme (Classic Win98) */
--desktop-bg: #008080;
--window-bg: #c0c0c0;
--window-text: #000000;
--border-light: #ffffff;
--border-dark: #000000;
--border-mid-light: #dfdfdf;
--border-mid-dark: #808080;
--title-bg: #000080;
--title-text: #ffffff;
--solitaire-bg: #008000;
--card-bg: #ffffff;
--card-text: #000000;
--card-border: #000000;
--input-bg: #ffffff;
}
[data-theme="dark"] {
/* Dark Theme (Nightmare/Hacker Win98) */
--desktop-bg: #0a0a0a;
--window-bg: #2b2b2b;
--window-text: #e0e0e0;
--border-light: #555555;
--border-dark: #000000;
--border-mid-light: #3a3a3a;
--border-mid-dark: #1a1a1a;
--title-bg: #000000;
--title-text: #00ff00;
--solitaire-bg: #002200;
--card-bg: #1a1a1a;
--card-text: #00ff00;
--card-border: #555555;
--input-bg: #111111;
}
/* --- GLOBAL STYLES --- */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'VT323', monospace;
user-select: none;
}
body {
background-color: var(--desktop-bg);
color: var(--window-text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
background-image: radial-gradient(circle, rgba(0,0,0,0.1) 1px, transparent 1px);
background-size: 4px 4px;
}
/* --- WIN98 UI COMPONENTS --- */
.window {
background-color: var(--window-bg);
border: 2px solid;
border-color: var(--border-light) var(--border-dark) var(--border-dark) var(--border-light);
padding: 2px;
box-shadow: inset 1px 1px var(--border-mid-light), inset -1px -1px var(--border-mid-dark);
display: flex;
flex-direction: column;
}
.title-bar {
background-color: var(--title-bg);
color: var(--title-text);
padding: 2px 4px;
font-size: 1.2rem;
display: flex;
justify-content: space-between;
align-items: center;
letter-spacing: 1px;
}
.title-bar-controls button {
background: var(--window-bg);
border: 1px solid;
border-color: var(--border-light) var(--border-dark) var(--border-dark) var(--border-light);
width: 16px;
height: 16px;
font-size: 0.8rem;
line-height: 10px;
text-align: center;
cursor: pointer;
color: var(--window-text);
font-weight: bold;
}
.title-bar-controls button:active {
border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark);
box-shadow: inset 1px 1px var(--border-mid-dark);
}
.window-content {
padding: 10px;
flex-grow: 1;
overflow-y: auto;
}
.btn {
background: var(--window-bg);
color: var(--window-text);
border: 2px solid;
border-color: var(--border-light) var(--border-dark) var(--border-dark) var(--border-light);
box-shadow: inset 1px 1px var(--border-mid-light), inset -1px -1px var(--border-mid-dark);
padding: 4px 12px;
font-size: 1.2rem;
cursor: pointer;
margin: 4px;
}
.btn:active {
border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark);
box-shadow: inset 1px 1px var(--border-mid-dark), inset -1px -1px var(--border-mid-light);
padding: 5px 11px 3px 13px; /* Shift text down */
}
input[type="text"], select {
background: var(--input-bg);
color: var(--window-text);
border: 2px solid;
border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark);
padding: 4px;
font-size: 1.2rem;
width: 100%;
margin-bottom: 10px;
outline: none;
}
/* --- LAYOUTS --- */
#desktop {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.top-bar {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
}
/* --- MODAL (LOGIN) --- */
#login-modal {
width: 350px;
z-index: 50;
}
.field-group {
margin-bottom: 15px;
}
.field-group label {
display: block;
margin-bottom: 5px;
font-size: 1.2rem;
}
/* --- MAIN APP WORKSPACE --- */
#app-workspace {
display: none; /* Hidden until login */
width: 100%;
height: 100%;
max-width: 1200px;
gap: 15px;
grid-template-columns: 2fr 1fr;
grid-template-rows: 2fr 1fr;
grid-template-areas:
"solitaire participants"
"results results";
}
/* --- SOLITAIRE AREA --- */
#solitaire-window {
grid-area: solitaire;
}
.solitaire-table {
background-color: var(--solitaire-bg);
height: 100%;
border: 2px solid;
border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark);
padding: 20px;
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
align-content: flex-start;
overflow-y: auto;
}
.poker-card {
background-color: var(--card-bg);
border: 2px solid var(--card-border);
border-radius: 6px;
width: 80px;
height: 110px;
display: flex;
justify-content: center;
align-items: center;
font-size: 2.5rem;
color: var(--card-text);
cursor: pointer;
position: relative;
transition: transform 0.1s ease;
box-shadow: 2px 2px 0px rgba(0,0,0,0.5);
}
.poker-card:hover {
transform: translateY(-5px);
}
.poker-card:active {
transform: translateY(0);
box-shadow: 1px 1px 0px rgba(0,0,0,0.5);
}
.poker-card::before, .poker-card::after {
content: attr(data-value);
position: absolute;
font-size: 1rem;
}
.poker-card::before { top: 5px; left: 5px; }
.poker-card::after { bottom: 5px; right: 5px; transform: rotate(180deg); }
/* --- PARTICIPANTS AREA --- */
#participants-window {
grid-area: participants;
}
.participant-list {
list-style: none;
background: var(--input-bg);
border: 2px solid;
border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark);
height: 100%;
padding: 5px;
overflow-y: auto;
}
.participant-item {
display: flex;
justify-content: space-between;
padding: 4px;
border-bottom: 1px dashed var(--border-mid-dark);
font-size: 1.2rem;
}
.status-voting { color: #888; font-style: italic; }
.status-voted { color: #00aa00; font-weight: bold; }
[data-theme="dark"] .status-voted { color: #00ff00; }
/* --- RESULTS AREA --- */
#results-window {
grid-area: results;
}
.results-table {
width: 100%;
border-collapse: collapse;
background: var(--input-bg);
border: 2px solid;
border-color: var(--border-dark) var(--border-light) var(--border-light) var(--border-dark);
}
.results-table th, .results-table td {
border: 1px solid var(--border-mid-dark);
padding: 8px;
text-align: left;
font-size: 1.2rem;
}
.results-table th {
background: var(--window-bg);
position: sticky;
top: 0;
box-shadow: inset 1px 1px var(--border-light), inset -1px -1px var(--border-dark);
}
.result-card-badge {
display: inline-block;
background: var(--card-bg);
color: var(--card-text);
border: 1px solid var(--card-border);
border-radius: 3px;
padding: 2px 8px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="top-bar">
<button class="btn" id="theme-toggle">🌙 Dark Mode</button>
</div>
<div id="desktop">
<div class="window" id="login-modal">
<div class="title-bar">
<span>Join.exe</span>
<div class="title-bar-controls">
<button>×</button>
</div>
</div>
<div class="window-content">
<form id="join-form">
<div class="field-group">
<label for="username">Name:</label>
<input type="text" id="username" autocomplete="off" required placeholder="Enter your alias...">
</div>
<div class="field-group">
<label for="role">Role:</label>
<select id="role">
<option value="participant">Participant</option>
<option value="viewer">Viewer</option>
</select>
</div>
<div style="text-align: right; margin-top: 20px;">
<button type="submit" class="btn">Connect</button>
</div>
</form>
</div>
</div>
<div id="app-workspace">
<div class="window" id="solitaire-window">
<div class="title-bar">
<span>ScrumPoker_Solitaire.exe</span>
<div class="title-bar-controls">
<button>_</button>
<button></button>
<button>×</button>
</div>
</div>
<div class="window-content solitaire-table" id="card-deck">
</div>
</div>
<div class="window" id="participants-window">
<div class="title-bar">
<span>Network Users</span>
</div>
<div class="window-content" style="padding: 2px;">
<ul class="participant-list" id="user-list">
<li class="participant-item">
<span>Alice_Dev</span>
<span class="status-voted">Voted</span>
</li>
<li class="participant-item">
<span>Bob_QA</span>
<span class="status-voting">Voting...</span>
</li>
<li class="participant-item">
<span>Charlie_PM</span>
<span class="status-voting">Viewer</span>
</li>
</ul>
</div>
</div>
<div class="window" id="results-window">
<div class="title-bar">
<span>Vote Results.log</span>
</div>
<div class="window-content" style="padding: 2px; overflow-y: auto;">
<table class="results-table">
<thead>
<tr>
<th style="width: 120px;">Card Picked</th>
<th>Participants</th>
</tr>
</thead>
<tbody id="results-body">
<tr>
<td><span class="result-card-badge">5</span></td>
<td>Alice_Dev, Dave_BE</td>
</tr>
<tr>
<td><span class="result-card-badge">8</span></td>
<td>Eve_Designer</td>
</tr>
<tr>
<td><span class="result-card-badge">?</span></td>
<td>Bob_QA</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
// --- THEME TOGGLE LOGIC ---
const themeToggleBtn = document.getElementById('theme-toggle');
let isDarkMode = false;
themeToggleBtn.addEventListener('click', () => {
isDarkMode = !isDarkMode;
if (isDarkMode) {
document.documentElement.setAttribute('data-theme', 'dark');
themeToggleBtn.textContent = '☀️ Light Mode';
} else {
document.documentElement.removeAttribute('data-theme');
themeToggleBtn.textContent = '🌙 Dark Mode';
}
});
// --- APP STATE ---
const fibonacciCards = ["0", "1", "2", "3", "5", "8", "13", "21", "?", "☕"];
let currentUser = { name: "", role: "participant", status: "Voting..." };
// --- DOM ELEMENTS ---
const loginModal = document.getElementById('login-modal');
const appWorkspace = document.getElementById('app-workspace');
const joinForm = document.getElementById('join-form');
const cardDeck = document.getElementById('card-deck');
const userList = document.getElementById('user-list');
// --- INITIALIZATION ---
function initDeck() {
cardDeck.innerHTML = '';
fibonacciCards.forEach(val => {
const card = document.createElement('div');
card.className = 'poker-card';
card.setAttribute('data-value', val);
card.textContent = val;
// Mock voting action
card.addEventListener('click', () => {
if(currentUser.role === 'viewer') {
alert("System Error: Viewers cannot vote.");
return;
}
alert(`You selected card: ${val}. In a real app, this would trigger a socket update!`);
// Mock UI update for user status
const myListItem = document.getElementById('my-user-item');
if(myListItem) {
myListItem.querySelector('.status').textContent = "Voted";
myListItem.querySelector('.status').className = "status status-voted";
}
});
cardDeck.appendChild(card);
});
}
// --- LOGIN / JOIN LOGIC ---
joinForm.addEventListener('submit', (e) => {
e.preventDefault();
const nameInput = document.getElementById('username').value.trim();
const roleInput = document.getElementById('role').value;
if (nameInput) {
currentUser.name = nameInput;
currentUser.role = roleInput;
currentUser.status = roleInput === 'viewer' ? 'Viewer' : 'Voting...';
// Transition UI
loginModal.style.display = 'none';
appWorkspace.style.display = 'grid'; // Enable CSS grid layout
// Render components
initDeck();
addUserToList();
}
});
function addUserToList() {
const li = document.createElement('li');
li.className = 'participant-item';
li.id = 'my-user-item';
const nameSpan = document.createElement('span');
nameSpan.innerHTML = `<strong>> ${currentUser.name} (You)</strong>`;
const statusSpan = document.createElement('span');
statusSpan.className = currentUser.role === 'viewer' ? 'status-voting' : 'status-voting status';
statusSpan.textContent = currentUser.status;
li.appendChild(nameSpan);
li.appendChild(statusSpan);
// Prepend so the current user is at the top of the list
userList.insertBefore(li, userList.firstChild);
// If viewer, dim the cards slightly to indicate they shouldn't click
if (currentUser.role === 'viewer') {
cardDeck.style.opacity = '0.6';
}
}
</script>
</body>
</html>