704 lines
No EOL
21 KiB
HTML
704 lines
No EOL
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="sv">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Live Resultat - Arbetsgrupper</title>
|
|
<script src="/socket.io/socket.io.js"></script>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
|
color: white;
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header {
|
|
text-align: center;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 10px;
|
|
background: linear-gradient(45deg, #3498db, #2ecc71);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.header p {
|
|
font-size: 1.2rem;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.dashboard {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr;
|
|
gap: 30px;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
align-items: start;
|
|
}
|
|
|
|
.card {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
backdrop-filter: blur(10px);
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.qr-section {
|
|
text-align: center;
|
|
min-width: 300px;
|
|
max-width: 350px;
|
|
}
|
|
|
|
.qr-section h2 {
|
|
margin-bottom: 20px;
|
|
color: #3498db;
|
|
}
|
|
|
|
.qr-code {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 15px;
|
|
display: inline-block;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.qr-code img {
|
|
max-width: 200px;
|
|
height: auto;
|
|
}
|
|
|
|
.url-display {
|
|
background: rgba(0, 0, 0, 0.3);
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
font-family: monospace;
|
|
word-break: break-all;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.results-section {
|
|
flex: 1;
|
|
}
|
|
|
|
.results-section h2 {
|
|
margin-bottom: 20px;
|
|
color: #2ecc71;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.connection-status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 6px 12px;
|
|
border-radius: 15px;
|
|
font-size: 0.8rem;
|
|
font-weight: bold;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.connection-status.connected {
|
|
background: #27ae60;
|
|
color: white;
|
|
}
|
|
|
|
.connection-status.disconnected {
|
|
background: #e74c3c;
|
|
color: white;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
margin-right: 6px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.connected .status-dot {
|
|
background: white;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.disconnected .status-dot {
|
|
background: white;
|
|
animation: none;
|
|
}
|
|
|
|
.floating-results {
|
|
position: relative;
|
|
min-height: 500px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
overflow: hidden;
|
|
display: flex;
|
|
gap: 15px;
|
|
}
|
|
|
|
.option-container {
|
|
padding: 15px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 12px;
|
|
border-left: 4px solid #3498db;
|
|
}
|
|
|
|
.option-text {
|
|
font-size: 1.3rem;
|
|
font-weight: 600;
|
|
color: #3498db;
|
|
margin-bottom: 15px;
|
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.participants-cloud {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.participant-cloud {
|
|
background: linear-gradient(45deg, #3498db, #2ecc71);
|
|
color: white;
|
|
padding: 6px 12px;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
|
|
animation: cloudFloat 3s ease-in-out infinite;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.participant-cloud:nth-child(2n) {
|
|
background: linear-gradient(45deg, #e74c3c, #f39c12);
|
|
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
|
|
animation-delay: -1s;
|
|
}
|
|
|
|
.participant-cloud:nth-child(3n) {
|
|
background: linear-gradient(45deg, #9b59b6, #e67e22);
|
|
box-shadow: 0 2px 8px rgba(155, 89, 182, 0.3);
|
|
animation-delay: -2s;
|
|
}
|
|
|
|
.participant-cloud:nth-child(4n) {
|
|
background: linear-gradient(45deg, #1abc9c, #27ae60);
|
|
box-shadow: 0 2px 8px rgba(26, 188, 156, 0.3);
|
|
animation-delay: -0.5s;
|
|
}
|
|
|
|
.participant-cloud:nth-child(5n) {
|
|
background: linear-gradient(45deg, #f39c12, #e67e22);
|
|
box-shadow: 0 2px 8px rgba(243, 156, 18, 0.3);
|
|
animation-delay: -1.5s;
|
|
}
|
|
|
|
@keyframes cloudFloat {
|
|
0%, 100% {
|
|
transform: translateY(0px) scale(1);
|
|
}
|
|
33% {
|
|
transform: translateY(-3px) scale(1.02);
|
|
}
|
|
66% {
|
|
transform: translateY(1px) scale(0.98);
|
|
}
|
|
}
|
|
|
|
.participant-cloud::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
animation: shimmer 4s infinite;
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% { left: -100%; }
|
|
100% { left: 100%; }
|
|
}
|
|
|
|
.option-count {
|
|
background: rgba(52, 152, 219, 0.8);
|
|
color: white;
|
|
padding: 4px 10px;
|
|
border-radius: 12px;
|
|
font-size: 0.8rem;
|
|
font-weight: bold;
|
|
margin-left: 10px;
|
|
display: inline-block;
|
|
}
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
height: 400px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
padding: 15px;
|
|
border-radius: 10px;
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #3498db;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.9rem;
|
|
opacity: 0.8;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.live-indicator {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
background: #e74c3c;
|
|
padding: 8px 15px;
|
|
border-radius: 20px;
|
|
font-size: 0.9rem;
|
|
font-weight: bold;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.live-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
background: white;
|
|
border-radius: 50%;
|
|
margin-right: 8px;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
.response-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 10px;
|
|
padding: 15px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.response-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px;
|
|
margin-bottom: 8px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.response-text {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.response-count {
|
|
background: #3498db;
|
|
color: white;
|
|
padding: 4px 12px;
|
|
border-radius: 15px;
|
|
font-size: 0.9rem;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.participants-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
margin-top: 20px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
padding: 20px;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
border-radius: 15px;
|
|
}
|
|
|
|
.participant-bubble {
|
|
background: linear-gradient(45deg, #3498db, #2ecc71);
|
|
color: white;
|
|
padding: 12px 18px;
|
|
border-radius: 25px;
|
|
font-size: 0.9rem;
|
|
font-weight: 500;
|
|
text-align: center;
|
|
min-width: 120px;
|
|
box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
|
|
animation: bubbleIn 0.5s ease-out;
|
|
transition: transform 0.3s ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.participant-bubble:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
|
|
}
|
|
|
|
.participant-bubble::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: -100%;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
animation: shimmer 2s infinite;
|
|
}
|
|
|
|
.participant-name {
|
|
font-weight: 600;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.participant-choice {
|
|
font-size: 0.8rem;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
@keyframes bubbleIn {
|
|
0% {
|
|
opacity: 0;
|
|
transform: scale(0.5) translateY(20px);
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes shimmer {
|
|
0% { left: -100%; }
|
|
100% { left: 100%; }
|
|
}
|
|
|
|
.bubble-colors {
|
|
--color1: #3498db;
|
|
--color2: #2ecc71;
|
|
}
|
|
|
|
.participant-bubble:nth-child(3n+1) {
|
|
background: linear-gradient(45deg, #e74c3c, #f39c12);
|
|
box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
|
|
}
|
|
|
|
.participant-bubble:nth-child(3n+2) {
|
|
background: linear-gradient(45deg, #9b59b6, #e67e22);
|
|
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
|
|
}
|
|
|
|
.participant-bubble:nth-child(3n+3) {
|
|
background: linear-gradient(45deg, #1abc9c, #27ae60);
|
|
box-shadow: 0 4px 15px rgba(26, 188, 156, 0.3);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.dashboard {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.qr-section {
|
|
max-width: 100%;
|
|
order: -1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.header h1 {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.card {
|
|
padding: 20px;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>
|
|
Vi behöver DIG! Vilken arbetsgrupp skulle du kunna tänka dig delta i?
|
|
</h1>
|
|
<p>
|
|
<span class="connection-status disconnected" id="connectionStatus">
|
|
<span class="status-dot"></span>
|
|
<span class="status-text">DISCONNECTED</span>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="dashboard">
|
|
<div class="card qr-section">
|
|
<h2>Scanna för att svara</h2>
|
|
|
|
<div class="qr-code" id="qrContainer">
|
|
<p>Laddar QR-kod...</p>
|
|
</div>
|
|
<div class="url-display" id="urlDisplay">
|
|
Laddar URL...
|
|
</div>
|
|
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="uniqueParticipants">0</div>
|
|
<div class="stat-label">Deltagare</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card results-section">
|
|
<h2>
|
|
Live Resultat
|
|
</h2>
|
|
|
|
<div class="floating-results" id="results">
|
|
<p style="text-align: center; opacity: 0.7;">Väntar på svar...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const socket = io();
|
|
let results = [];
|
|
let allResponses = [];
|
|
let availableOptions = [];
|
|
let participantColour = {};
|
|
|
|
// Chart colors
|
|
const colors = [
|
|
'#3498db', '#2ecc71', '#e74c3c', '#f39c12',
|
|
'#9b59b6', '#1abc9c', '#34495e', '#e67e22',
|
|
'#95a5a6', '#c0392b', '#8e44ad', '#27ae60'
|
|
];
|
|
|
|
function updateResults() {
|
|
// Show a container box for each option, with the option text and a count of responses
|
|
// In each box, show the list of participants who chose that option
|
|
|
|
const resultsContainer = document.getElementById('results');
|
|
resultsContainer.innerHTML = ''; // Clear previous results
|
|
|
|
results.forEach(result => {
|
|
const optionContainer = document.createElement('div');
|
|
optionContainer.classList.add('option-container');
|
|
optionContainer.setAttribute('data-option', result.response);
|
|
|
|
const optionText = document.createElement('h3');
|
|
optionText.classList.add('option-text');
|
|
optionText.textContent = result.response;
|
|
|
|
const optionCount = document.createElement('span');
|
|
optionCount.classList.add('option-count');
|
|
optionCount.textContent = result.count;
|
|
|
|
optionText.appendChild(optionCount);
|
|
optionContainer.appendChild(optionText);
|
|
|
|
|
|
const participantsCloud = document.createElement('div');
|
|
participantsCloud.classList.add('participants-cloud');
|
|
|
|
optionContainer.appendChild(participantsCloud);
|
|
|
|
resultsContainer.appendChild(optionContainer);
|
|
});
|
|
|
|
allResponses.forEach(response => {
|
|
const participantsCloud = document.querySelector(`.option-container[data-option="${response.response}"] .participants-cloud`);
|
|
if (participantsCloud) {
|
|
const participantBubble = document.createElement('div');
|
|
participantBubble.classList.add('participant-cloud');
|
|
participantBubble.innerText = response.participant_name;
|
|
|
|
participantsCloud.appendChild(participantBubble);
|
|
}
|
|
});
|
|
|
|
console.log('Available options updated:', availableOptions);
|
|
|
|
availableOptions.forEach(option => {
|
|
if (!results.some(r => r.response === option)) {
|
|
const optionContainer = document.createElement('div');
|
|
optionContainer.classList.add('option-container');
|
|
optionContainer.setAttribute('data-option', option);
|
|
|
|
const optionText = document.createElement('h3');
|
|
optionText.classList.add('option-text');
|
|
optionText.textContent = option;
|
|
|
|
const optionCount = document.createElement('span');
|
|
optionCount.classList.add('option-count');
|
|
optionCount.textContent = '0';
|
|
|
|
optionText.appendChild(optionCount);
|
|
optionContainer.appendChild(optionText);
|
|
|
|
resultsContainer.appendChild(optionContainer);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateStats() {
|
|
const uniqueParticipants = new Set(allResponses.map(r => r.participant_name)).size;
|
|
|
|
// Update participants count
|
|
const participantCountElement = document.querySelector('.stat-card:last-child .stat-value');
|
|
participantCountElement.textContent = uniqueParticipants;
|
|
}
|
|
|
|
function loadQRCode() {
|
|
fetch('/api/qr')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.qrCode) {
|
|
const qrContainer = document.getElementById('qrContainer');
|
|
qrContainer.innerHTML = `<img src="${data.qrCode}" alt="QR Code" style="max-width: 100%; height: auto;">`;
|
|
|
|
const urlDisplay = document.getElementById('urlDisplay');
|
|
urlDisplay.textContent = window.location.origin;
|
|
} else {
|
|
throw new Error('No QR code data received');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading QR code:', error);
|
|
document.getElementById('qrContainer').innerHTML = `<p style="color: #e74c3c;">Fel vid laddning av QR-kod: ${error.message}</p>`;
|
|
});
|
|
}
|
|
|
|
// Socket.IO event listeners
|
|
socket.on('resultsUpdated', (newResults) => {
|
|
results = newResults;
|
|
updateResults();
|
|
updateStats();
|
|
});
|
|
|
|
socket.on('allResponsesUpdated', (newAllResponses) => {
|
|
allResponses = newAllResponses;
|
|
|
|
allResponses.forEach(response => {
|
|
if (!participantColour[response.participant_name]) {
|
|
const colourIndex = Object.keys(participantColour).length % colors.length;
|
|
participantColour[response.participant_name] = colors[colourIndex];
|
|
}
|
|
});
|
|
|
|
updateResults();
|
|
updateStats();
|
|
});
|
|
|
|
// Socket.IO event listeners
|
|
socket.on('optionsUpdated', (options) => {
|
|
availableOptions = options;
|
|
|
|
updateResults();
|
|
updateStats();
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
document.getElementById('connectionStatus').classList.remove('connected');
|
|
document.getElementById('connectionStatus').classList.add('disconnected');
|
|
document.getElementById('connectionStatus').querySelector('.status-text').innerText = 'DISCONNECTED';
|
|
});
|
|
|
|
socket.io.on('error', () => {
|
|
document.getElementById('connectionStatus').classList.remove('connected');
|
|
document.getElementById('connectionStatus').classList.add('disconnected');
|
|
document.getElementById('connectionStatus').querySelector('.status-text').innerText = 'DISCONNECTED';
|
|
});
|
|
|
|
socket.on('connect', () => {
|
|
document.getElementById('connectionStatus').classList.remove('disconnected');
|
|
document.getElementById('connectionStatus').classList.add('connected');
|
|
document.getElementById('connectionStatus').querySelector('.status-text').innerText = 'LIVE';
|
|
});
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadQRCode();
|
|
|
|
// Load initial data
|
|
fetch('/api/results')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
results = data;
|
|
updateResults();
|
|
updateStats();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading initial results:', error);
|
|
});
|
|
|
|
fetch('/api/all-responses')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
allResponses = data;
|
|
updateResults();
|
|
updateStats();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading initial responses:', error);
|
|
});
|
|
|
|
|
|
fetch('/api/options')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
availableOptions = options;
|
|
updateResults();
|
|
updateStats();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading options:', error);
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |