diff --git a/assets/css/custom.css b/assets/css/custom.css index afeabe1..4e2c525 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -274,10 +274,20 @@ body { 100% { color: inherit; } } +@keyframes blink-theme { + 0% { color: inherit; } + 50% { color: var(--accent-color); } + 100% { color: inherit; } +} + .timer-alert { animation: blink-red 0.5s infinite; } +.timer-finish-theme { + animation: blink-theme 0.5s infinite; +} + /* Settings Switches */ .form-check-input:checked { background-color: var(--accent-color); @@ -357,4 +367,11 @@ body.dark-mode .text-muted { /* Saved Timers Hover Effect */ #saved-timers-list .card-precise:hover { border-color: var(--success-color); +} + +/* Color input styling */ +.participant-color-input { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + cursor: pointer; } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 28304da..8d097cf 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -26,6 +26,7 @@ const app = { // Relay State currentParticipant: 1, + participantNames: [], // Custom State customActivities: [ @@ -53,7 +54,7 @@ const app = { 'countdown': { title: 'Countdown Timer', allowPause: true, hasLaps: false, forceCountdown: true, canSave: true }, 'stopwatch': { title: 'Stopwatch', allowPause: false, hasLaps: false, canToggleCountdown: false, canSave: true }, 'lap': { title: 'Lap Timer', allowPause: false, hasLaps: true, canToggleCountdown: false }, - 'relay': { title: 'Relay Timer', allowPause: true, hasRelay: true, canToggleCountdown: false }, + 'relay': { title: 'Relay Timer', allowPause: false, hasRelay: true, canToggleCountdown: false }, 'custom': { title: 'Custom Timer', allowPause: true, isCustom: true, canToggleCountdown: true, canSave: true } }, @@ -78,6 +79,7 @@ const app = { btnNext: document.getElementById('btn-next'), btnSaveMain: document.getElementById('btn-save-main'), btnSaveBuilder: document.getElementById('btn-save-session-builder'), + btnResetBuilder: document.getElementById('btn-reset-builder'), formatSelect: document.getElementById('format-select'), listTitle: document.getElementById('list-title'), @@ -92,7 +94,12 @@ const app = { inputS: document.getElementById('input-s'), relayConfig: document.getElementById('relay-config'), + relayParticipantCountBox: document.getElementById('relay-participant-count-box'), participantCount: document.getElementById('participant-count'), + participantNamesContainer: document.getElementById('participant-names-container'), + relayActiveParticipantContainer: document.getElementById('relay-active-participant-container'), + relayActiveName: document.getElementById('relay-active-name'), + btnRelaySplit: document.getElementById('btn-relay-split'), customBuilder: document.getElementById('custom-builder'), activityList: document.getElementById('activity-list'), @@ -102,6 +109,7 @@ const app = { btnDeleteSaved: document.getElementById('btn-delete-saved'), // Settings / Options + optionsDropdown: document.getElementById('optionsDropdown'), optCountdownContainer: document.getElementById('opt-countdown-container'), customOptions: document.getElementById('custom-options'), restAlertContainer: document.getElementById('rest-alert-container'), @@ -115,6 +123,8 @@ const app = { alertPreEndSec: document.getElementById('alert-pre-end-seconds'), alertCompletion: document.getElementById('alert-completion'), + preFinishSection: document.getElementById('pre-finish-section'), + brandLink: document.getElementById('brand-link'), darkModeToggle: document.getElementById('dark-mode-toggle') }, @@ -153,9 +163,14 @@ const app = { this.el.btnReset.addEventListener('click', () => this.resetTimer()); this.el.btnLap.addEventListener('click', () => this.recordLap()); this.el.btnNext.addEventListener('click', () => this.recordRelaySplit()); + this.el.btnRelaySplit.addEventListener('click', () => this.recordRelaySplit()); this.el.btnSaveMain.addEventListener('click', () => this.saveCurrentTimer()); this.el.btnSaveBuilder.addEventListener('click', () => this.saveCurrentTimer()); + if (this.el.btnResetBuilder) { + this.el.btnResetBuilder.addEventListener('click', () => this.resetCustomBuilder()); + } + this.el.brandLink.addEventListener('click', (e) => { e.preventDefault(); this.switchView('landing'); @@ -166,6 +181,8 @@ const app = { this.el.settingIsCountdown.addEventListener('change', () => this.resetTimer()); this.el.lapSort.addEventListener('change', () => this.renderHistory()); + this.el.participantCount.addEventListener('input', () => this.renderParticipantInputs()); + this.el.savedTimersDropdown.addEventListener('change', (e) => { if (e.target.value) { this.loadTimer(e.target.value); @@ -226,7 +243,7 @@ const app = { // UI Adjustments this.el.btnPause.classList.toggle('d-none', !mode.allowPause); this.el.btnLap.classList.toggle('d-none', !mode.hasLaps); - this.el.btnNext.classList.toggle('d-none', !mode.hasRelay); + this.el.btnNext.classList.add('d-none'); // Removed from timer entirely for all modes this.el.btnSaveMain.classList.toggle('d-none', !mode.canSave || mode.isCustom); this.el.timerSideColumn.classList.toggle('d-none', !mode.hasLaps && !mode.hasRelay && !mode.isCustom); @@ -235,17 +252,37 @@ const app = { this.el.customBuilder.classList.remove('d-none'); this.el.historySection.classList.add('d-none'); this.el.savedTimersContainer.classList.remove('d-none'); + this.el.relayConfig.classList.add('d-none'); + } else if (mode.hasRelay) { + this.el.customBuilder.classList.add('d-none'); + this.el.relayConfig.classList.remove('d-none'); + this.el.historySection.classList.remove('d-none'); + this.el.savedTimersContainer.classList.add('d-none'); + + // Move history to main column immediately for Relay (separate from participants) + this.el.lapResultsContainer.classList.remove('d-none'); + this.el.lapResultsContainer.appendChild(this.el.historySection); + this.el.lapSort.classList.remove('d-none'); } else { this.el.customBuilder.classList.add('d-none'); + this.el.relayConfig.classList.add('d-none'); this.el.historySection.classList.remove('d-none'); this.el.savedTimersContainer.classList.toggle('d-none', !mode.canSave); } this.el.countdownInputs.classList.toggle('d-none', modeKey !== 'countdown'); - this.el.relayConfig.classList.toggle('d-none', !mode.hasRelay); this.el.customOptions.classList.toggle('d-none', !mode.isCustom); this.el.restAlertContainer.classList.toggle('d-none', !mode.isCustom); this.el.activeActivityName.classList.add('d-none'); + this.el.relayActiveParticipantContainer.classList.add('d-none'); + + // Options Dropdown visibility logic + this.el.optionsDropdown.classList.toggle('d-none', modeKey === 'time-watch'); + + // Pre-finish section visibility logic + const noPreFinishModes = ['stopwatch', 'lap', 'relay']; + this.el.preFinishSection.classList.toggle('d-none', noPreFinishModes.includes(modeKey)); + this.el.lapSort.classList.add('d-none'); // Options Box adjustments @@ -267,7 +304,8 @@ const app = { this.el.listHead.innerHTML = 'LapSplit TimeDeltaNotes'; } else if (mode.hasRelay) { this.el.listTitle.textContent = 'Relay Splits'; - this.el.listHead.innerHTML = 'ParticipantTotal TimeSplitStatus'; + this.el.listHead.innerHTML = 'ParticipantStart TimeSplit TimeTotal Time'; + this.renderParticipantInputs(); } else if (mode.isCustom) { this.el.listTitle.textContent = 'Activities'; this.el.listHead.innerHTML = 'ActivityDurationStatus'; @@ -277,6 +315,25 @@ const app = { this.updateDisplay(); }, + renderParticipantInputs() { + const count = Math.min(12, Math.max(1, parseInt(this.el.participantCount.value) || 1)); + this.el.participantCount.value = count; + + let html = ''; + const colors = ['#3b82f6', '#10b981', '#ef4444', '#f59e0b', '#8b5cf6', '#ec4899', '#06b6d4', '#f97316', '#14b8a6', '#6366f1', '#a855f7', '#d946ef']; + for (let i = 1; i <= count; i++) { + const defaultColor = colors[(i - 1) % colors.length]; + html += ` +
+ #${i} + + +
+ `; + } + this.el.participantNamesContainer.innerHTML = html; + }, + handleStartClick() { if (!this.el.sessionTitle.value.trim()) { if (this.currentMode === 'custom') { @@ -348,6 +405,7 @@ const app = { } this.countdownStartValue = totalMs; this.elapsedTime = totalMs; + this.el.countdownInputs.classList.add('d-none'); } if (mode.isCustom && !this.isPaused) { @@ -371,6 +429,14 @@ const app = { this.updateCurrentActivityStatus('in-progress'); } + if (mode.hasRelay && !this.isPaused) { + this.currentParticipant = 1; + this.history = []; + this.el.relayActiveParticipantContainer.classList.remove('d-none'); + this.el.relayParticipantCountBox.classList.add('d-none'); + this.updateRelayActiveDisplay(); + } + if (this.isPaused) { const offset = (isCountdownMode) ? (this.countdownStartValue - this.elapsedTime) : this.elapsedTime; this.startTime = now - offset; @@ -388,6 +454,7 @@ const app = { this.el.btnReset.disabled = true; this.el.sessionTitle.disabled = true; this.el.mainTimer.classList.remove('timer-finish'); + this.el.mainTimer.classList.remove('timer-finish-theme'); this.el.mainTimer.classList.remove('timer-alert'); // Lock inputs @@ -395,6 +462,8 @@ const app = { if (this.el.inputM) this.el.inputM.disabled = true; if (this.el.inputS) this.el.inputS.disabled = true; if (this.el.participantCount) this.el.participantCount.disabled = true; + document.querySelectorAll('.participant-name-input').forEach(i => i.disabled = true); + document.querySelectorAll('.participant-color-input').forEach(i => i.disabled = true); this.tick(); }, @@ -428,6 +497,11 @@ const app = { this.el.btnStop.disabled = true; this.el.btnReset.disabled = false; this.el.mainTimer.classList.remove('timer-alert'); + this.el.relayActiveParticipantContainer.classList.add('d-none'); + + if (this.currentMode === 'countdown') { + this.el.countdownInputs.classList.remove('d-none'); + } if (this.currentMode === 'lap') { this.recordLap(); @@ -436,7 +510,10 @@ const app = { this.el.lapResultsContainer.appendChild(this.el.historySection); this.el.lapSort.classList.remove('d-none'); } else if (this.currentMode === 'relay') { - this.recordRelaySplit(true); + if (wasRunning) this.recordRelaySplit(true); + // Relay splits panel is already under controls in setMode, just ensure sorting is visible + this.el.lapResultsContainer.classList.remove('d-none'); + this.el.lapSort.classList.remove('d-none'); } else if (this.currentMode === 'custom' && wasRunning) { this.updateCurrentActivityStatus('Canceled'); } @@ -454,39 +531,76 @@ const app = { this.el.listBody.innerHTML = ''; this.el.mainTimer.classList.remove('timer-finish'); + this.el.mainTimer.classList.remove('timer-finish-theme'); this.el.mainTimer.classList.remove('timer-alert'); this.el.btnReset.disabled = true; this.el.btnStart.disabled = false; this.el.btnPause.disabled = true; this.el.btnStop.disabled = true; + this.el.btnNext.disabled = false; + this.el.btnRelaySplit.disabled = false; this.el.activeActivityName.classList.add('d-none'); + this.el.relayActiveParticipantContainer.classList.add('d-none'); + this.el.relayParticipantCountBox.classList.remove('d-none'); this.el.sessionTitle.disabled = false; - if (this.el.inputH) this.el.inputH.disabled = false; - if (this.el.inputM) this.el.inputM.disabled = false; - if (this.el.inputS) this.el.inputS.disabled = false; + if (this.el.inputH) { + this.el.inputH.disabled = false; + if (this.currentMode === 'countdown') this.el.inputH.value = ''; + } + if (this.el.inputM) { + this.el.inputM.disabled = false; + if (this.currentMode === 'countdown') this.el.inputM.value = ''; + } + if (this.el.inputS) { + this.el.inputS.disabled = false; + if (this.currentMode === 'countdown') this.el.inputS.value = ''; + } + + if (this.currentMode === 'countdown' || this.currentMode === 'stopwatch') { + this.el.savedTimersDropdown.value = ''; + this.el.sessionTitle.value = ''; + if (this.currentMode) this.sessionTitles[this.currentMode] = ''; + } + if (this.el.participantCount) this.el.participantCount.disabled = false; + document.querySelectorAll('.participant-name-input').forEach(i => i.disabled = false); + document.querySelectorAll('.participant-color-input').forEach(i => i.disabled = false); + + if (this.currentMode === 'countdown') { + this.el.countdownInputs.classList.remove('d-none'); + } + + // Move history back to side column default + this.el.timerSideColumn.querySelector('.card-precise').appendChild(this.el.historySection); + this.el.lapResultsContainer.classList.add('d-none'); + this.el.lapSort.classList.add('d-none'); // Transition: History -> Builder (if custom) if (this.currentMode === 'custom') { this.el.customBuilder.classList.remove('d-none'); this.el.historySection.classList.add('d-none'); this.el.savedTimersContainer.classList.remove('d-none'); - // Ensure history section is back in side column if it was moved (though only lap moves it) - this.el.timerSideColumn.querySelector('.card-precise').appendChild(this.el.historySection); - } - - if (this.currentMode === 'lap') { - // Move history back to side column - this.el.timerSideColumn.querySelector('.card-precise').appendChild(this.el.historySection); - this.el.lapResultsContainer.classList.add('d-none'); - this.el.lapSort.classList.add('d-none'); + } else if (this.currentMode === 'relay') { + // Put it back under timer for relay even after reset + this.el.lapResultsContainer.classList.remove('d-none'); + this.el.lapResultsContainer.appendChild(this.el.historySection); + this.el.lapSort.classList.remove('d-none'); } this.updateDisplay(); }, + resetCustomBuilder() { + this.customActivities = [{ name: 'Activity 1', duration: 60000, isRest: false }]; + this.el.sessionTitle.value = ''; + if (this.currentMode) this.sessionTitles[this.currentMode] = ''; + this.el.savedTimersDropdown.value = ''; + this.renderCustomBuilder(); + this.resetTimer(); + }, + tick() { if (!this.isRunning || this.isPaused) return; @@ -599,8 +713,13 @@ const app = { updateDisplay() { const ms = this.elapsedTime; - const format = this.el.formatSelect.value; + let format = this.el.formatSelect.value; + if (this.currentMode === 'time-watch') { + if (format === 'hh:mm:ss.ms') format = 'hh:mm:ss'; + if (format === 'seconds.ms') format = 'seconds'; + } + this.el.mainTimer.textContent = this.formatTime(ms, format); // Only show sub-timer if format is not milliseconds as requested @@ -656,6 +775,24 @@ const app = { this.renderHistory(); }, + updateRelayActiveDisplay() { + const maxParticipants = parseInt(this.el.participantCount.value) || 1; + if (this.currentParticipant > maxParticipants) { + this.el.relayActiveParticipantContainer.classList.add('d-none'); + return; + } + + const nameInput = document.querySelector(`.participant-name-input[data-index="${this.currentParticipant}"]`); + const name = nameInput ? (nameInput.value.trim() || `P${this.currentParticipant}`) : `P${this.currentParticipant}`; + const colorInput = document.querySelector(`.participant-color-input[data-index="${this.currentParticipant}"]`); + const color = colorInput ? colorInput.value : '#3b82f6'; + + this.el.relayActiveName.textContent = name; + this.el.relayActiveName.style.color = color; + this.el.btnRelaySplit.disabled = false; + this.el.btnRelaySplit.textContent = 'Split'; + }, + recordRelaySplit(isFinal = false) { const now = this.elapsedTime; const lastTime = this.history.length > 0 ? this.history[this.history.length - 1].time : 0; @@ -664,28 +801,44 @@ const app = { if (this.currentParticipant > maxParticipants && !isFinal) return; + // Get name and color + const nameInput = document.querySelector(`.participant-name-input[data-index="${this.currentParticipant}"]`); + let name = nameInput ? (nameInput.value.trim() || `P${this.currentParticipant}`) : `P${this.currentParticipant}`; + const colorInput = document.querySelector(`.participant-color-input[data-index="${this.currentParticipant}"]`); + const color = colorInput ? colorInput.value : '#3b82f6'; + + // If it's the last participant, mark as final but keep their name + if (this.currentParticipant === maxParticipants) { + isFinal = true; + } + this.history.push({ - id: `P${this.currentParticipant}`, + id: name, + color: color, + startTime: lastTime, time: now, delta: delta, status: isFinal ? 'DONE' : 'SPLIT' }); + this.el.btnRelaySplit.disabled = true; + if (!isFinal) { this.currentParticipant++; - if (this.currentParticipant > maxParticipants) { - this.stopTimer(); - } + this.updateRelayActiveDisplay(); + } else { + this.stopTimer(); + this.el.btnRelaySplit.disabled = true; } this.renderHistory(); }, renderHistory() { - if (this.currentMode === 'lap') { - let sortedHistory = this.history.slice(); - const sortVal = this.el.lapSort.value; + let sortedHistory = this.history.slice(); + const sortVal = this.el.lapSort.value; + if (this.currentMode === 'lap' || this.currentMode === 'relay') { if (sortVal === 'best') { sortedHistory.sort((a, b) => a.delta - b.delta); } else if (sortVal === 'worse') { @@ -699,27 +852,27 @@ const app = { if (h.delta < minDelta) minDelta = h.delta; }); - this.el.listBody.innerHTML = sortedHistory.map(h => ` - - #${h.id} - ${this.formatTime(h.time, 'hh:mm:ss.ms')} - +${this.formatTime(h.delta, 'seconds.ms')} - - ${h.delta === minDelta ? 'BEST' : ''} - - - `).join(''); - } else if (this.currentMode === 'relay') { - this.el.listBody.innerHTML = this.history.slice().reverse().map(h => ` - - ${h.id} - ${this.formatTime(h.time, 'hh:mm:ss.ms')} - ${this.formatTime(h.delta, 'seconds.ms')} - - ${h.status} - - - `).join(''); + if (this.currentMode === 'relay') { + this.el.listBody.innerHTML = sortedHistory.map(h => ` + + ${h.id} + ${this.formatTime(h.startTime || 0, 'hh:mm:ss.ms')} + ${this.formatTime(h.delta, 'hh:mm:ss.ms')} + ${this.formatTime(h.time, 'hh:mm:ss.ms')} + + `).join(''); + } else { + this.el.listBody.innerHTML = sortedHistory.map(h => ` + + ${h.id} + ${this.formatTime(h.time, 'hh:mm:ss.ms')} + +${this.formatTime(h.delta, 'seconds.ms')} + + ${h.delta === minDelta && h.delta > 0 ? 'BEST' : ''} + + + `).join(''); + } } else if (this.currentMode === 'custom') { this.el.listBody.innerHTML = this.history.slice().reverse().map(h => { let badgeClass = 'bg-success'; @@ -884,11 +1037,7 @@ const app = { alert('Session saved successfully!'); if (this.currentMode === 'custom') { - this.customActivities = [{ name: 'Activity 1', duration: 60000, isRest: false }]; - this.el.sessionTitle.value = ''; - if (this.currentMode) this.sessionTitles[this.currentMode] = ''; - this.renderCustomBuilder(); - this.resetTimer(); + this.resetCustomBuilder(); } } else { alert('Error saving session: ' + result.error); @@ -997,7 +1146,11 @@ const app = { }, playCompletionAlert() { - this.el.mainTimer.classList.add('timer-finish'); + this.el.mainTimer.classList.add('timer-finish-theme'); + setTimeout(() => { + this.el.mainTimer.classList.remove('timer-finish-theme'); + }, 3000); + if (this.el.alertCompletion.checked) { this.beep(880, 0.5); setTimeout(() => this.beep(1100, 0.5), 200); diff --git a/index.php b/index.php index 20e2b15..66d4d38 100644 --- a/index.php +++ b/index.php @@ -158,7 +158,7 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-
+
@@ -169,7 +169,7 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-
+
@@ -187,7 +187,7 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-
+
@@ -199,7 +199,7 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
- +
@@ -238,6 +238,7 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+
@@ -246,6 +247,18 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+ +
+

Participants

+
+ + +
+
+ +
+
+
@@ -269,6 +282,14 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
Activity Name
+ + +
+
+ Participant Name + +
+
00:00:00.000
@@ -299,15 +320,6 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; - - -
-
- - -
-
-