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 = '
| Lap | Split Time | Delta | Notes |
';
} else if (mode.hasRelay) {
this.el.listTitle.textContent = 'Relay Splits';
- this.el.listHead.innerHTML = '| Participant | Total Time | Split | Status |
';
+ this.el.listHead.innerHTML = '| Participant | Start Time | Split Time | Total Time |
';
+ this.renderParticipantInputs();
} else if (mode.isCustom) {
this.el.listTitle.textContent = 'Activities';
this.el.listHead.innerHTML = '| Activity | Duration | Status |
';
@@ -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'] ?? '';
-
+
-
+
-
+
+
+
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'] ?? '';
-
-
-
-
-
-
-
-
-