Files
pointtrackerv2/public/index.html

1470 lines
47 KiB
HTML
Raw 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>Point Tracker</title>
<script src="/socket.io/socket.io.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%);
min-height: 100vh;
padding: 20px;
color: #e0e0e0;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: #2a2a2a;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
overflow: hidden;
}
.header {
background: linear-gradient(45deg, #404040, #606060);
color: white;
padding: 20px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.connection-status {
position: absolute;
top: 10px;
right: 10px;
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
}
.connection-status.connected {
background: #4CAF50;
color: white;
}
.connection-status.disconnected {
background: #f44336;
color: white;
}
.main-content {
padding: 30px;
}
.admin-bar {
background: #1a1a1a;
padding: 15px;
margin-bottom: 30px;
border-radius: 10px;
border: 2px solid #404040;
display: none;
}
.admin-bar.active {
display: block;
}
.admin-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.admin-controls span {
color: #ff6b6b;
font-weight: bold;
margin-right: 15px;
}
.points-section {
margin-bottom: 40px;
}
.section-title {
font-size: 1.5em;
margin-bottom: 20px;
color: #cccccc;
font-weight: bold;
}
.points-grid {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 20px;
align-items: center;
}
.point-bubble {
width: 80px;
height: 80px;
border-radius: 50%;
border: 3px solid #404040;
background: #2a2a2a;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
transition: all 0.3s ease;
position: relative;
color: #e0e0e0;
}
.point-bubble:hover {
transform: scale(1.05);
}
.point-bubble.available {
border-color: #4CAF50;
background: #1a2e1a;
color: #4CAF50;
}
.point-bubble.used {
border-color: #f44336;
background: #2e1a1a;
color: #f44336;
}
.point-bubble.active {
border-color: #ff9800;
background: #2e261a;
color: #ff9800;
animation: pulse 2s infinite;
}
.point-bubble.paused {
border-color: #2196f3;
background: #1a222e;
color: #2196f3;
}
.point-bubble.disabled {
border-color: #666;
background: #333;
color: #666;
cursor: not-allowed;
opacity: 0.5;
}
.add-point-btn {
width: 60px;
height: 60px;
border-radius: 50%;
border: 3px dashed #4CAF50;
background: #1a2e1a;
color: #4CAF50;
font-size: 24px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.add-point-btn:hover {
transform: scale(1.05);
background: #2a4a2a;
}
.admin-bar.active ~ .main-content .add-point-btn {
display: flex;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 152, 0, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(255, 152, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 152, 0, 0); }
}
.timer-display {
position: absolute;
top: -25px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
color: #cccccc;
padding: 5px 10px;
border-radius: 15px;
font-size: 12px;
white-space: nowrap;
border: 1px solid #404040;
}
.admin-overlay {
position: absolute;
top: -5px;
right: -5px;
display: none;
}
.admin-bar.active ~ .main-content .admin-overlay {
display: block;
}
.admin-btn {
width: 20px;
height: 20px;
border-radius: 50%;
border: none;
cursor: pointer;
font-size: 12px;
margin: 1px;
}
.admin-btn.expire {
background: #ff4757;
color: white;
}
.admin-btn.remove {
background: #ff3838;
color: white;
}
.timer-controls {
margin-top: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
}
.btn-primary {
background: #404040;
color: white;
}
.btn-primary:hover {
background: #505050;
}
.btn-warning {
background: #ff9800;
color: white;
}
.btn-warning:hover {
background: #f57c00;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-success {
background: #4CAF50;
color: white;
}
.btn-success:hover {
background: #388e3c;
}
.btn-small {
padding: 5px 10px;
font-size: 12px;
}
.admin-section {
border-top: 2px solid #404040;
padding-top: 30px;
margin-top: 30px;
}
.log-section {
background: #1a1a1a;
padding: 20px;
border-radius: 10px;
margin-top: 30px;
border: 1px solid #404040;
}
.log-entry {
background: #2a2a2a;
padding: 15px;
margin-bottom: 10px;
border-radius: 8px;
border-left: 4px solid #cccccc;
}
.log-entry:last-child {
margin-bottom: 0;
}
.log-time {
font-size: 0.9em;
color: #999;
margin-bottom: 5px;
}
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.modal.no-close {
pointer-events: auto;
}
.modal-content {
background-color: #2a2a2a;
margin: 15% auto;
padding: 30px;
border-radius: 15px;
width: 90%;
max-width: 500px;
border: 2px solid #404040;
position: relative;
}
.modal.no-close .modal-content {
pointer-events: auto;
}
.modal h2 {
margin-bottom: 20px;
color: #cccccc;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #e0e0e0;
}
.form-group input, .form-group select {
width: 100%;
padding: 10px;
border: 2px solid #404040;
border-radius: 8px;
font-size: 16px;
background: #1a1a1a;
color: #e0e0e0;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #cccccc;
}
.daily-limit {
background: #2e261a;
border: 1px solid #ff9800;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
color: #e0e0e0;
}
.daily-limit.exceeded {
background: #2e1a1a;
border-color: #f44336;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-available { background: #4CAF50; }
.status-used { background: #f44336; }
.status-active { background: #ff9800; }
.status-paused { background: #2196f3; }
.solo-limit-warning {
background: #2e1a1a;
border: 1px solid #f44336;
padding: 10px;
border-radius: 8px;
margin-bottom: 15px;
color: #f44336;
text-align: center;
display: none;
}
.solo-limit-warning.active {
display: block;
}
.quick-add {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.custom-timer-section {
background: #1a1a1a;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
border: 1px solid #404040;
}
.time-input-group {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.time-input {
width: 60px;
padding: 5px;
border: 1px solid #404040;
border-radius: 4px;
background: #2a2a2a;
color: #e0e0e0;
text-align: center;
}
.episode-movie-section {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #404040;
}
.episode-movie-section .btn {
margin-bottom: 10px;
}
.point-selection-mode {
background: #1a2e1a !important;
border-color: #4CAF50 !important;
animation: selectPulse 1s infinite;
}
.point-selected {
background: #2e4a2e !important;
border-color: #66ff66 !important;
box-shadow: 0 0 10px rgba(102, 255, 102, 0.5);
}
@keyframes selectPulse {
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); }
70% { box-shadow: 0 0 0 5px rgba(76, 175, 80, 0); }
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
}
.selection-instructions {
background: #1a2e1a;
border: 2px solid #4CAF50;
color: #4CAF50;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
text-align: center;
display: none;
}
.selection-instructions.active {
display: block;
}
</style>
</head>
<body>
<div class="connection-status disconnected" id="connectionStatus">Disconnected</div>
<div class="container">
<div class="header">
<h1>🎯 Point Tracker</h1>
<p>Manage your solo and together points</p>
</div>
<!-- Admin Bar -->
<div class="admin-bar" id="adminBar">
<div class="admin-controls">
<span>🔧 ADMIN MODE ACTIVE</span>
<div class="quick-add">
<button class="btn btn-success btn-small" onclick="quickAddPoint('solo')">+Solo</button>
<button class="btn btn-success btn-small" onclick="quickAddPoint('together')">+Together</button>
<button class="btn btn-primary btn-small" onclick="showCustomTimerModal()">+Custom</button>
<button class="btn btn-warning btn-small" onclick="clearLog()">Clear Log</button>
<button class="btn btn-warning btn-small" onclick="resetDay()">Reset Daily Limit</button>
<button class="btn btn-danger btn-small" onclick="resetAllPoints()">Reset All</button>
<button class="btn btn-warning btn-small" onclick="testAudio()">Test Audio</button>
<button class="btn btn-primary btn-small" onclick="exitAdminMode()">Exit Admin</button>
</div>
</div>
</div>
<div class="main-content">
<!-- Solo Points Section -->
<div class="points-section">
<div class="section-title">Solo Points (15 minutes each) - One at a time</div>
<div class="solo-limit-warning" id="soloLimitWarning">
⚠️ You can only run one solo point at a time. Finish or stop the current timer first.
</div>
<div class="selection-instructions" id="selectionInstructions">
<div id="selectionText">Click on points to select them</div>
<button class="btn btn-success btn-small" onclick="confirmSelection()" id="confirmBtn" style="margin-left: 10px;">Confirm Selection</button>
<button class="btn btn-danger btn-small" onclick="cancelSelection()" style="margin-left: 5px;">Cancel</button>
</div>
<div class="points-grid" id="soloPoints">
<!-- Solo point bubbles will be generated here -->
</div>
<div class="add-point-btn" onclick="quickAddPoint('solo')" title="Add Solo Point">+</div>
<!-- Episode/Movie Actions -->
<div class="episode-movie-section">
<div class="timer-controls">
<button class="btn btn-warning" onclick="startEpisodeSelection()" id="episodeBtn">📺 Watch Episode (2 solo = 30min)</button>
<button class="btn btn-warning" onclick="startMovieSelection()" id="movieBtn">🎬 Watch Movie (4 solo = 2hr)</button>
</div>
</div>
</div>
<!-- Together Points Section -->
<div class="points-section">
<div class="section-title">Together Points (1 hour each)</div>
<div class="daily-limit" id="togetherLimit">
<strong>Daily Limit:</strong> <span id="togetherUsedToday">0</span>/1 together points used today
</div>
<div class="points-grid" id="togetherPoints">
<!-- Together point bubbles will be generated here -->
</div>
<div class="add-point-btn" onclick="quickAddPoint('together')" title="Add Together Point">+</div>
</div>
<!-- Admin Controls -->
<div class="admin-section">
<div class="section-title">Management</div>
<div class="timer-controls">
<button class="btn btn-primary" onclick="showAddPointsModal()">Add Points</button>
<button class="btn btn-primary" onclick="showAdminModal()">Admin Panel</button>
</div>
</div>
<!-- Activity Log -->
<div class="log-section">
<div class="section-title">Recent Activity (Last 5 uses)</div>
<div id="activityLog">
<p style="color: #999;">No activity yet...</p>
</div>
</div>
</div>
</div>
<!-- Add Points Modal -->
<div id="addPointsModal" class="modal">
<div class="modal-content">
<h2>Add Points</h2>
<div class="form-group">
<label>Password:</label>
<input type="password" id="addPointsPassword" placeholder="Enter password">
</div>
<div class="form-group">
<label>Point Type:</label>
<select id="pointType">
<option value="solo">Solo Points (15 min)</option>
<option value="together">Together Points (1 hour)</option>
</select>
</div>
<div class="form-group">
<label>Number of Points:</label>
<input type="number" id="pointCount" min="1" max="10" value="1">
</div>
<div class="timer-controls">
<button class="btn btn-success" onclick="addPoints()">Add Points</button>
<button class="btn btn-danger" onclick="closeModal('addPointsModal')">Cancel</button>
</div>
</div>
</div>
<!-- Admin Panel Modal -->
<div id="adminModal" class="modal">
<div class="modal-content">
<h2>Admin Panel</h2>
<div class="form-group">
<label>Admin Password:</label>
<input type="password" id="adminPassword" placeholder="Enter admin password">
</div>
<div class="timer-controls">
<button class="btn btn-primary" onclick="adminLogin()">Enter Admin Mode</button>
<button class="btn btn-danger" onclick="closeModal('adminModal')">Cancel</button>
</div>
</div>
</div>
<!-- Custom Timer Modal -->
<div id="customTimerModal" class="modal">
<div class="modal-content">
<h2>Create Custom Timer</h2>
<div class="form-group">
<label>Timer Name:</label>
<input type="text" id="customTimerName" placeholder="e.g., Quick Task, Deep Work" maxlength="20">
</div>
<div class="form-group">
<label>Duration:</label>
<div class="time-input-group">
<input type="number" id="customHours" class="time-input" min="0" max="5" value="0" placeholder="H">
<span>hours</span>
<input type="number" id="customMinutes" class="time-input" min="0" max="59" value="30" placeholder="M">
<span>minutes</span>
</div>
</div>
<div class="form-group">
<label>Point Type:</label>
<select id="customPointType">
<option value="solo">Solo Type (one at a time)</option>
<option value="together">Together Type (daily limit)</option>
<option value="custom">Custom Type (no limits)</option>
</select>
</div>
<div class="timer-controls">
<button class="btn btn-success" onclick="createCustomTimer()">Create Timer</button>
<button class="btn btn-danger" onclick="closeModal('customTimerModal')">Cancel</button>
</div>
</div>
</div>
<!-- Custom Dialog Modal -->
<div id="customDialog" class="modal">
<div class="modal-content" style="max-width: 400px;">
<h2 id="dialogTitle">Confirm</h2>
<p id="dialogMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
<div class="timer-controls">
<button class="btn btn-success" id="dialogConfirm">Yes</button>
<button class="btn btn-danger" id="dialogCancel">No</button>
</div>
</div>
</div>
<!-- Alert Modal -->
<div id="alertModal" class="modal">
<div class="modal-content" style="max-width: 400px;">
<h2 id="alertTitle">Alert</h2>
<p id="alertMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
<div class="timer-controls">
<button class="btn btn-primary" onclick="closeModal('alertModal')">OK</button>
</div>
</div>
</div>
<!-- Finish-Up Modal -->
<div id="finishUpModal" class="modal no-close">
<div class="modal-content" style="max-width: 400px;">
<h2>🔔 Timer Completed!</h2>
<p id="finishUpMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
<p style="color: #e0e0e0;">Would you like 2 minutes of finish-up time?</p>
<div class="timer-controls">
<button class="btn btn-success" id="finishUpYes">Yes</button>
<button class="btn btn-danger" id="finishUpNo">No</button>
</div>
</div>
</div>
<!-- Timer Complete Modal -->
<div id="timerCompleteModal" class="modal no-close">
<div class="modal-content" style="max-width: 400px;">
<h2>🔔 Timer Completed!</h2>
<p id="timerCompleteMessage" style="margin: 20px 0; color: #e0e0e0;"></p>
<div class="timer-controls">
<button class="btn btn-primary" onclick="dismissTimerComplete()">Dismiss</button>
</div>
</div>
</div>
<!-- Audio element for timer sounds -->
<audio id="timerAudio" preload="auto" loop>
<source src="timer-complete.mp3" type="audio/mpeg">
<source src="timer-complete.wav" type="audio/wav">
<source src="timer-complete.ogg" type="audio/ogg">
</audio>
<script>
// Socket.IO connection
const socket = io();
// Client state
let gameState = {
soloPoints: [],
togetherPoints: [],
customPoints: [],
activeTimers: {},
pausedTimers: {},
activityLog: [],
togetherUsedToday: 0,
lastResetDate: new Date().toDateString(),
adminMode: false,
selectionMode: null,
selectedPoints: []
};
// Connection status
socket.on('connect', () => {
console.log('Connected to server');
document.getElementById('connectionStatus').textContent = 'Connected';
document.getElementById('connectionStatus').className = 'connection-status connected';
});
socket.on('disconnect', () => {
console.log('Disconnected from server');
document.getElementById('connectionStatus').textContent = 'Disconnected';
document.getElementById('connectionStatus').className = 'connection-status disconnected';
});
// Receive initial state
socket.on('initialState', (state) => {
console.log('Received initial state:', state);
gameState = { ...gameState, ...state };
renderPoints();
updateTogetherLimit();
renderLog();
updateSoloLimitWarning();
});
// Receive state updates
socket.on('stateUpdate', (state) => {
console.log('State update received:', state);
gameState = { ...gameState, ...state };
renderPoints();
updateTogetherLimit();
renderLog();
updateSoloLimitWarning();
});
// Timer updates
socket.on('timerUpdate', (data) => {
const { pointId, remainingSeconds, type } = data;
updateTimerDisplay(pointId, remainingSeconds, type);
});
// Timer completion
socket.on('timerComplete', (data) => {
const { pointId, type, offerFinishUp } = data;
if (offerFinishUp) {
// Play sound and show finish-up modal
startAlarmSound();
const point = findPoint(pointId);
const pointName = point ? (point.customName || point.type) : 'Timer';
document.getElementById('finishUpMessage').textContent = `${pointName} point completed!`;
document.getElementById('finishUpModal').style.display = 'block';
// Set up button handlers
document.getElementById('finishUpYes').onclick = () => {
stopAlarmSound();
socket.emit('finishUpResponse', { pointId, accepted: true });
closeModal('finishUpModal');
};
document.getElementById('finishUpNo').onclick = () => {
stopAlarmSound();
socket.emit('finishUpResponse', { pointId, accepted: false });
closeModal('finishUpModal');
};
} else if (type === 'finishUp') {
// Finish-up completed - show completion dialog
startAlarmSound();
const point = findPoint(pointId);
const pointName = point ? (point.customName || point.type) : 'Timer';
document.getElementById('timerCompleteMessage').textContent = `${pointName} point finish-up time completed!`;
document.getElementById('timerCompleteModal').style.display = 'block';
}
});
// Admin auth success
socket.on('adminAuthSuccess', () => {
gameState.adminMode = true;
document.getElementById('adminBar').classList.add('active');
closeModal('adminModal');
});
socket.on('adminAuthFailed', () => {
alert('Incorrect admin password!');
});
// Custom dialog and alert functions
function showAlert(message, title = 'Alert') {
document.getElementById('alertTitle').textContent = title;
document.getElementById('alertMessage').textContent = message;
document.getElementById('alertModal').style.display = 'block';
}
function showConfirm(message, title = 'Confirm') {
return new Promise((resolve) => {
document.getElementById('dialogTitle').textContent = title;
document.getElementById('dialogMessage').textContent = message;
document.getElementById('customDialog').style.display = 'block';
document.getElementById('dialogConfirm').onclick = () => {
closeModal('customDialog');
resolve(true);
};
document.getElementById('dialogCancel').onclick = () => {
closeModal('customDialog');
resolve(false);
};
});
}
// Error handling
socket.on('error', (data) => {
showAlert(data.message, 'Error');
});
function renderPoints() {
renderSoloPoints();
renderTogetherPoints();
renderCustomPoints();
}
function renderSoloPoints() {
const container = document.getElementById('soloPoints');
container.innerHTML = '';
gameState.soloPoints.forEach(point => {
const bubble = createPointBubble(point);
container.appendChild(bubble);
});
}
function renderTogetherPoints() {
const container = document.getElementById('togetherPoints');
container.innerHTML = '';
gameState.togetherPoints.forEach(point => {
const bubble = createPointBubble(point);
container.appendChild(bubble);
});
}
function renderCustomPoints() {
gameState.customPoints.forEach(point => {
let container;
if (point.type === 'solo') {
container = document.getElementById('soloPoints');
} else if (point.type === 'together') {
container = document.getElementById('togetherPoints');
} else {
container = document.getElementById('soloPoints');
}
const bubble = createPointBubble(point);
container.appendChild(bubble);
});
}
function createPointBubble(point) {
const bubble = document.createElement('div');
const isDisabled = isPointDisabled(point);
let className = `point-bubble ${isDisabled ? 'disabled' : point.status}`;
// Add selection mode classes
if (gameState.selectionMode && point.type === 'solo' && point.status === 'available') {
className += ' point-selection-mode';
if (gameState.selectedPoints.includes(point.id)) {
className += ' point-selected';
}
}
bubble.className = className;
bubble.innerHTML = getPointLabel(point);
// Handle clicks based on mode
if (gameState.selectionMode && point.type === 'solo' && point.status === 'available') {
bubble.onclick = () => togglePointSelection(point.id);
} else if (!isDisabled) {
if (point.status === 'available') {
bubble.onclick = () => usePoint(point.id);
} else if (point.status === 'active') {
bubble.onclick = () => pauseTimer(point.id);
} else if (point.status === 'paused') {
bubble.onclick = () => resumeTimer(point.id);
}
}
// Add timer display if active or paused
if (point.status === 'active' || point.status === 'paused') {
const timerDisplay = document.createElement('div');
timerDisplay.className = 'timer-display';
timerDisplay.id = `timer_${point.id}`;
bubble.appendChild(timerDisplay);
}
// Add admin controls (only if not in selection mode)
if (gameState.adminMode && !gameState.selectionMode) {
const adminOverlay = document.createElement('div');
adminOverlay.className = 'admin-overlay';
if (point.status === 'active' || point.status === 'paused') {
const expireBtn = document.createElement('button');
expireBtn.className = 'admin-btn expire';
expireBtn.innerHTML = '⏰';
expireBtn.title = 'Expire Timer (Trigger Ring)';
expireBtn.onclick = (e) => {
e.stopPropagation();
expireTimer(point.id);
};
adminOverlay.appendChild(expireBtn);
}
const removeBtn = document.createElement('button');
removeBtn.className = 'admin-btn remove';
removeBtn.innerHTML = '×';
removeBtn.title = 'Remove Point';
removeBtn.onclick = (e) => {
e.stopPropagation();
removePoint(point.id);
};
adminOverlay.appendChild(removeBtn);
bubble.appendChild(adminOverlay);
}
return bubble;
}
function isPointDisabled(point) {
if ((point.type === 'solo' || point.customType === 'solo') && point.status === 'available') {
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
return allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
}
return false;
}
function getPointLabel(point) {
if (point.customName) {
switch (point.status) {
case 'available': return point.label;
case 'used': return '✓';
case 'active': return '⏱️';
case 'paused': return '⏸️';
default: return '?';
}
}
switch (point.status) {
case 'available':
return point.label || (point.type === 'solo' ? '15m' : '1h');
case 'used':
return '✓';
case 'active':
return '⏱️';
case 'paused':
return '⏸️';
default:
return '?';
}
}
function findPoint(pointId) {
return [...gameState.soloPoints, ...gameState.togetherPoints, ...gameState.customPoints]
.find(p => p.id === pointId);
}
// User actions
function usePoint(pointId) {
socket.emit('usePoint', { pointId });
}
function pauseTimer(pointId) {
socket.emit('pauseTimer', { pointId });
}
function resumeTimer(pointId) {
socket.emit('resumeTimer', { pointId });
}
function updateTimerDisplay(pointId, remainingSeconds, type) {
const display = document.getElementById(`timer_${pointId}`);
if (display) {
const prefix = type === 'finishUp' ? 'Finish: ' : '';
display.textContent = prefix + formatTime(remainingSeconds);
}
}
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${mins.toString().padStart(2, '0')}h`;
} else {
return `${mins}:${secs.toString().padStart(2, '0')}`;
}
}
function updateSoloLimitWarning() {
const warning = document.getElementById('soloLimitWarning');
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
if (hasActiveSolo) {
warning.classList.add('active');
} else {
warning.classList.remove('active');
}
updateEpisodeMovieButtons();
}
function updateEpisodeMovieButtons() {
const episodeBtn = document.getElementById('episodeBtn');
const movieBtn = document.getElementById('movieBtn');
if (!episodeBtn || !movieBtn) return;
const availableSoloCount = gameState.soloPoints.filter(p => p.status === 'available').length;
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
episodeBtn.disabled = hasActiveSolo || availableSoloCount < 2 || gameState.selectionMode;
movieBtn.disabled = hasActiveSolo || availableSoloCount < 4 || gameState.selectionMode;
if (episodeBtn.disabled) {
episodeBtn.style.opacity = '0.5';
episodeBtn.style.cursor = 'not-allowed';
} else {
episodeBtn.style.opacity = '1';
episodeBtn.style.cursor = 'pointer';
}
if (movieBtn.disabled) {
movieBtn.style.opacity = '0.5';
movieBtn.style.cursor = 'not-allowed';
} else {
movieBtn.style.opacity = '1';
movieBtn.style.cursor = 'pointer';
}
}
function updateTogetherLimit() {
const element = document.getElementById('togetherUsedToday');
const container = document.getElementById('togetherLimit');
element.textContent = gameState.togetherUsedToday;
if (gameState.togetherUsedToday >= 1) {
container.classList.add('exceeded');
} else {
container.classList.remove('exceeded');
}
}
function renderLog() {
const container = document.getElementById('activityLog');
if (gameState.activityLog.length === 0) {
container.innerHTML = '<p style="color: #999;">No activity yet...</p>';
return;
}
container.innerHTML = gameState.activityLog.map(entry => {
const timestamp = new Date(entry.timestamp);
return `
<div class="log-entry">
<div class="log-time">${timestamp.toLocaleString()}</div>
<div>${entry.message}</div>
</div>
`;
}).join('');
}
// Episode/Movie selection
function startEpisodeSelection() {
const availableCount = gameState.soloPoints.filter(p => p.status === 'available').length;
if (availableCount < 2) {
showAlert('You need at least 2 available solo points to watch an episode.');
return;
}
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
if (hasActiveSolo) {
showAlert('You can only run one solo timer at a time. Finish or stop the current timer first.');
return;
}
gameState.selectionMode = 'episode';
gameState.selectedPoints = [];
document.getElementById('selectionInstructions').classList.add('active');
document.getElementById('selectionText').textContent = 'Select 2 solo points for episode (30 minutes)';
renderPoints();
}
function startMovieSelection() {
const availableCount = gameState.soloPoints.filter(p => p.status === 'available').length;
if (availableCount < 4) {
showAlert('You need at least 4 available solo points to watch a movie.');
return;
}
const allPoints = [...gameState.soloPoints, ...gameState.customPoints];
const hasActiveSolo = allPoints.some(p => (p.type === 'solo' || p.customType === 'solo') && (p.status === 'active' || p.status === 'paused'));
if (hasActiveSolo) {
showAlert('You can only run one solo timer at a time. Finish or stop the current timer first.');
return;
}
gameState.selectionMode = 'movie';
gameState.selectedPoints = [];
document.getElementById('selectionInstructions').classList.add('active');
document.getElementById('selectionText').textContent = 'Select 4 solo points for movie (2 hours)';
renderPoints();
}
function togglePointSelection(pointId) {
const requiredCount = gameState.selectionMode === 'episode' ? 2 : 4;
if (gameState.selectedPoints.includes(pointId)) {
gameState.selectedPoints = gameState.selectedPoints.filter(id => id !== pointId);
} else {
if (gameState.selectedPoints.length < requiredCount) {
gameState.selectedPoints.push(pointId);
}
}
const selectedCount = gameState.selectedPoints.length;
const modeText = gameState.selectionMode === 'episode' ? 'episode' : 'movie';
document.getElementById('selectionText').textContent =
`Select ${requiredCount} solo points for ${modeText} (${selectedCount}/${requiredCount} selected)`;
renderPoints();
}
async function confirmSelection() {
const requiredCount = gameState.selectionMode === 'episode' ? 2 : 4;
if (gameState.selectedPoints.length !== requiredCount) {
showAlert(`Please select exactly ${requiredCount} points.`);
return;
}
const modeText = gameState.selectionMode === 'episode' ? 'episode' : 'movie';
const duration = gameState.selectionMode === 'episode' ? '30 minutes' : '2 hours';
const confirmed = await showConfirm(`Use ${requiredCount} selected solo points to watch ${modeText} (${duration})?`);
if (!confirmed) return;
socket.emit('confirmSelection', {
selectedPoints: gameState.selectedPoints,
type: gameState.selectionMode
});
cancelSelection();
}
function cancelSelection() {
gameState.selectionMode = null;
gameState.selectedPoints = [];
document.getElementById('selectionInstructions').classList.remove('active');
renderPoints();
}
// Admin functions
function showAdminModal() {
document.getElementById('adminModal').style.display = 'block';
}
function adminLogin() {
const password = document.getElementById('adminPassword').value;
socket.emit('adminAuth', { password });
}
function exitAdminMode() {
gameState.adminMode = false;
document.getElementById('adminBar').classList.remove('active');
renderPoints();
}
function quickAddPoint(type) {
if (!gameState.adminMode) return;
socket.emit('quickAddPoint', { type });
}
function expireTimer(pointId) {
if (!gameState.adminMode) return;
socket.emit('expireTimer', { pointId });
}
async function removePoint(pointId) {
if (!gameState.adminMode) return;
const confirmed = await showConfirm('Remove this point permanently?');
if (!confirmed) return;
socket.emit('removePoint', { pointId });
}
async function resetAllPoints() {
if (!gameState.adminMode) return;
const confirmed = await showConfirm('Reset all points to 5 solo and 4 together? This will stop all timers and remove custom points.');
if (!confirmed) return;
socket.emit('resetAllPoints');
}
async function resetDay() {
if (!gameState.adminMode) return;
const confirmed = await showConfirm('Reset daily together point limit? This will allow using another together point today.');
if (!confirmed) return;
socket.emit('resetDailyLimit');
}
function clearLog() {
if (!gameState.adminMode) return;
socket.emit('clearLog');
}
// Add points modal
function showAddPointsModal() {
document.getElementById('addPointsModal').style.display = 'block';
}
function addPoints() {
const password = document.getElementById('addPointsPassword').value;
const type = document.getElementById('pointType').value;
const count = parseInt(document.getElementById('pointCount').value);
socket.emit('addPoints', { password, type, count });
closeModal('addPointsModal');
}
// Custom timer modal
function showCustomTimerModal() {
if (!gameState.adminMode) return;
document.getElementById('customTimerModal').style.display = 'block';
}
function createCustomTimer() {
const name = document.getElementById('customTimerName').value.trim();
const hours = parseInt(document.getElementById('customHours').value) || 0;
const minutes = parseInt(document.getElementById('customMinutes').value) || 0;
const pointType = document.getElementById('customPointType').value;
if (!name) {
showAlert('Please enter a timer name');
return;
}
if (hours === 0 && minutes === 0) {
showAlert('Please set a duration greater than 0');
return;
}
socket.emit('createCustomTimer', { name, hours, minutes, pointType });
closeModal('customTimerModal');
}
// Modal functions
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
const modal = document.getElementById(modalId);
const inputs = modal.querySelectorAll('input, select');
inputs.forEach(input => {
if (input.type === 'password' || input.type === 'text') {
input.value = '';
} else if (input.type === 'number') {
input.value = input.getAttribute('value') || '';
} else if (input.tagName === 'SELECT') {
input.selectedIndex = 0;
}
});
}
// Audio functions
let audioPlaying = false;
function startAlarmSound() {
console.log('Starting alarm sound...');
const audioElement = document.getElementById('timerAudio');
// Force load the audio
audioElement.load();
// Set volume to max
audioElement.volume = 1.0;
audioElement.loop = true;
// Try to play
const playPromise = audioElement.play();
if (playPromise !== undefined) {
playPromise
.then(() => {
console.log('Audio playing successfully');
audioPlaying = true;
})
.catch(error => {
console.error('Audio playback failed:', error);
console.log('Audio src:', audioElement.src);
console.log('Audio readyState:', audioElement.readyState);
console.log('Audio error:', audioElement.error);
// Try fallback beep
createBeepSound();
});
}
}
function stopAlarmSound() {
console.log('Stopping alarm sound...');
const audioElement = document.getElementById('timerAudio');
audioElement.pause();
audioElement.currentTime = 0;
audioPlaying = false;
// Stop any fallback beep
if (window.audioContext) {
window.audioContext.close();
window.audioContext = null;
}
}
function playCompletionSound() {
console.log('Playing completion sound (single)...');
const audioElement = document.getElementById('timerAudio');
audioElement.loop = false;
audioElement.volume = 1.0;
audioElement.play().catch(error => {
console.error('Single play failed:', error);
});
}
function createBeepSound() {
console.log('Creating fallback beep sound...');
try {
window.audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = window.audioContext.createOscillator();
const gainNode = window.audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(window.audioContext.destination);
oscillator.frequency.value = 800; // Frequency in Hz
oscillator.type = 'sine';
gainNode.gain.value = 0.3; // Volume
oscillator.start();
audioPlaying = true;
// Create beeping pattern
const beepPattern = () => {
if (!audioPlaying) {
oscillator.stop();
window.audioContext.close();
window.audioContext = null;
return;
}
gainNode.gain.value = gainNode.gain.value > 0 ? 0 : 0.3;
setTimeout(beepPattern, 500);
};
beepPattern();
console.log('Beep sound started');
} catch (e) {
console.error('Web Audio API error:', e);
// Last resort - use notification API if available
if ('Notification' in window && Notification.permission === 'granted') {
new Notification('Timer Complete!', {
body: 'Your timer has finished.',
requireInteraction: true
});
}
}
}
// Test audio on page load
window.addEventListener('load', () => {
const audioElement = document.getElementById('timerAudio');
console.log('Audio element found:', audioElement);
console.log('Audio sources:', audioElement.getElementsByTagName('source').length);
// Check if audio can play
audioElement.addEventListener('canplay', () => {
console.log('Audio can play');
});
audioElement.addEventListener('error', (e) => {
console.error('Audio error event:', e);
});
// Request notification permission for fallback
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
});
function dismissTimerComplete() {
stopAlarmSound();
closeModal('timerCompleteModal');
}
// Test audio function
function testAudio() {
console.log('Testing audio...');
startAlarmSound();
// Show test modal
showAlert('Testing audio alarm. Click OK to stop.', 'Audio Test');
// Override OK button to stop sound
setTimeout(() => {
const okButton = document.querySelector('#alertModal button');
okButton.onclick = () => {
stopAlarmSound();
closeModal('alertModal');
};
}, 100);
}
// Close modals when clicking outside (except for no-close modals)
window.onclick = function(event) {
const modals = document.querySelectorAll('.modal:not(.no-close)');
modals.forEach(modal => {
if (event.target === modal) {
modal.style.display = 'none';
}
});
};
</script>
</body>
</html>