diff --git a/assets/css/custom.css b/assets/css/custom.css index 1dd57f4..c3b7c60 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -11,11 +11,31 @@ --font-mono: 'JetBrains Mono', 'Fira Code', monospace; } +body.dark-mode { + --bg-color: #0f172a; + --surface-color: #1e293b; + --text-primary: #f8fafc; + --text-secondary: #94a3b8; + --border-color: #334155; + --accent-color: #60a5fa; + --accent-hover: #93c5fd; +} + body { background-color: var(--bg-color); color: var(--text-primary); font-family: 'Inter', system-ui, -apple-system, sans-serif; -webkit-font-smoothing: antialiased; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.navbar { + background-color: var(--bg-color) !important; + border-color: var(--border-color) !important; +} + +.navbar-brand { + color: var(--text-primary) !important; } .font-tabular { @@ -23,6 +43,25 @@ body { font-family: var(--font-mono); } +/* Dark Mode Toggle */ +.dark-mode-toggle { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--border-color); + background: var(--surface-color); + cursor: pointer; + transition: all 0.2s ease; +} + +.dark-mode-toggle:hover { + border-color: var(--accent-color); + color: var(--accent-color); +} + /* Landing Clock */ .hero-clock { font-size: clamp(4rem, 15vw, 10rem); @@ -38,11 +77,12 @@ body { border: 1px solid var(--border-color); border-radius: 4px; transition: all 0.2s ease; + color: var(--text-primary); } .card-precise:hover { border-color: var(--accent-color); - background: #fff; + background: var(--bg-color); box-shadow: 0 4px 12px rgba(0,0,0,0.03); } @@ -69,6 +109,10 @@ body { } /* Table */ +.table { + color: var(--text-primary); +} + .table-precise { font-size: 0.875rem; } @@ -82,6 +126,10 @@ body { border-bottom: 2px solid var(--border-color); } +.table-precise td { + border-bottom: 1px solid var(--border-color); +} + .badge-best { background-color: var(--success-color); color: white; @@ -100,6 +148,19 @@ body { } /* Forms */ +.form-control, .form-select { + background-color: var(--surface-color); + color: var(--text-primary); + border-color: var(--border-color); +} + +.form-control:focus, .form-select:focus { + background-color: var(--bg-color); + color: var(--text-primary); + border-color: var(--accent-color); + box-shadow: none; +} + .form-control-precise { border-radius: 4px; border: 1px solid var(--border-color); @@ -107,13 +168,8 @@ body { padding: 0.4rem 0.75rem; } -.form-control-precise:focus { - border-color: var(--accent-color); - box-shadow: none; -} - #session-title { - background-color: var(--surface-color); + background-color: transparent; border-width: 0 0 2px 0; border-radius: 0; padding-left: 0; @@ -133,8 +189,21 @@ body { } .activity-row { + background: var(--bg-color) !important; + border-color: var(--border-color) !important; transition: transform 0.2s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.02); + cursor: grab; +} + +.activity-row:active { + cursor: grabbing; +} + +.activity-row.dragging { + opacity: 0.5; + background: var(--border-color) !important; + transform: scale(0.98); } .activity-row:hover { @@ -142,10 +211,33 @@ body { box-shadow: 0 4px 8px rgba(0,0,0,0.05); } +.activity-row.rest-activity { + border-left: 4px solid var(--accent-color) !important; + font-size: 0.8rem; + opacity: 0.9; + padding: 0.5rem 1rem !important; +} + +.activity-row.rest-activity .form-control { + font-size: 0.8rem; +} + +.activity-row.rest-activity .btn { + font-size: 0.65rem; + padding: 0.2rem 0.4rem; +} + +.activity-row.rest-activity .fs-tiny { + font-size: 0.6rem; +} + /* Options Dropdown */ .dropdown-menu { - box-shadow: 0 10px 25px rgba(0,0,0,0.1) !important; + background-color: var(--surface-color); + color: var(--text-primary); + box-shadow: 0 10px 25px rgba(0,0,0,0.2) !important; border: 1px solid var(--border-color); + z-index: 1050; } .dropdown-header { @@ -161,6 +253,12 @@ body { .fs-tiny { font-size: 0.65rem; } .cursor-pointer { cursor: pointer; } +#active-activity-name { + letter-spacing: 0.02em; + font-size: 1.1rem; + font-weight: 600; +} + /* Alerts / Finishes */ .timer-finish { animation: flash 1s infinite; @@ -176,4 +274,40 @@ body { .form-check-input:checked { background-color: var(--accent-color); border-color: var(--accent-color); +} + +/* Layout Adjustments */ +.timer-workspace-container { + display: flex; + flex-wrap: wrap; + gap: 2rem; + justify-content: center; +} + +.timer-main-column { + flex: 1; + min-width: 320px; + max-width: 800px; + order: 2; /* Main on right on desktop */ +} + +.timer-side-column { + width: 350px; + order: 1; /* History on left on desktop */ +} + +@media (max-width: 1200px) { + .timer-side-column { + width: 100%; + max-width: 800px; + margin: 0 auto; + order: 2; /* Move history back to bottom on mobile */ + } + .timer-main-column { + order: 1; /* Main timer on top on mobile */ + } +} + +footer { + border-color: var(--border-color) !important; } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index ec5d31f..b4359ae 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -6,6 +6,7 @@ * - Display Formats * - Lap, Relay Split & Custom Activity Logic * - Sound Alerts & Session Management + * - Dark Mode & UI Layout */ const app = { @@ -15,6 +16,7 @@ const app = { isRunning: false, isPaused: false, isPreStarting: false, + isDarkMode: false, startTime: 0, elapsedTime: 0, pausedTime: 0, @@ -27,10 +29,11 @@ const app = { // Custom State customActivities: [ - { name: 'Activity 1', duration: 60000 }, - { name: 'Activity 2', duration: 120000 } + { name: 'Activity 1', duration: 60000, isRest: false }, + { name: 'Rest', duration: 30000, isRest: true } ], currentActivityIndex: 0, + draggedItemIndex: null, // Audio Helpers audioCtx: null, @@ -56,6 +59,7 @@ const app = { timerTitle: document.getElementById('timer-title'), activeActivityName: document.getElementById('active-activity-name'), sessionTitle: document.getElementById('session-title'), + timerSideColumn: document.getElementById('timer-side-column'), btnStart: document.getElementById('btn-start'), btnPause: document.getElementById('btn-pause'), @@ -65,7 +69,6 @@ const app = { btnNext: document.getElementById('btn-next'), formatSelect: document.getElementById('format-select'), - listContainer: document.getElementById('list-container'), listTitle: document.getElementById('list-title'), listHead: document.getElementById('list-head'), listBody: document.getElementById('list-body'), @@ -84,24 +87,48 @@ const app = { // Settings / Options optCountdownContainer: document.getElementById('opt-countdown-container'), + customOptions: document.getElementById('custom-options'), + restAlertContainer: document.getElementById('rest-alert-container'), settingIsCountdown: document.getElementById('setting-is-countdown'), + settingRestDuration: document.getElementById('setting-rest-duration'), + alertRestPreEnd: document.getElementById('alert-rest-pre-end'), + alertPreStart: document.getElementById('alert-pre-start'), alertPreStartSec: document.getElementById('alert-pre-start-seconds'), alertPreEnd: document.getElementById('alert-pre-end'), alertPreEndSec: document.getElementById('alert-pre-end-seconds'), alertCompletion: document.getElementById('alert-completion'), - brandLink: document.getElementById('brand-link') + brandLink: document.getElementById('brand-link'), + darkModeToggle: document.getElementById('dark-mode-toggle') }, init() { console.log('Timer App Initializing...'); + this.initDarkMode(); this.updateLandingClock(); this.bindEvents(); this.renderCustomBuilder(); this.resetTimer(); }, + initDarkMode() { + this.isDarkMode = localStorage.getItem('darkMode') === 'true'; + this.applyDarkMode(); + }, + + applyDarkMode() { + document.body.classList.toggle('dark-mode', this.isDarkMode); + document.getElementById('moon-icon').classList.toggle('d-none', this.isDarkMode); + document.getElementById('sun-icon').classList.toggle('d-none', !this.isDarkMode); + }, + + toggleDarkMode() { + this.isDarkMode = !this.isDarkMode; + localStorage.setItem('darkMode', this.isDarkMode); + this.applyDarkMode(); + }, + bindEvents() { this.el.btnStart.addEventListener('click', () => this.handleStartClick()); this.el.btnPause.addEventListener('click', () => this.pauseTimer()); @@ -116,6 +143,7 @@ const app = { this.switchView('landing'); }); + this.el.darkModeToggle.addEventListener('click', () => this.toggleDarkMode()); this.el.formatSelect.addEventListener('change', () => this.updateDisplay()); this.el.settingIsCountdown.addEventListener('change', () => this.resetTimer()); }, @@ -157,10 +185,12 @@ const app = { this.el.btnLap.classList.toggle('d-none', !mode.hasLaps); this.el.btnNext.classList.toggle('d-none', !mode.hasRelay); - this.el.listContainer.classList.toggle('d-none', !mode.hasLaps && !mode.hasRelay && !mode.isCustom); + this.el.timerSideColumn.classList.toggle('d-none', !mode.hasLaps && !mode.hasRelay && !mode.isCustom); this.el.countdownInputs.classList.toggle('d-none', modeKey !== 'countdown'); this.el.relayConfig.classList.toggle('d-none', !mode.hasRelay); this.el.customBuilder.classList.toggle('d-none', !mode.isCustom); + 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'); // Options Box adjustments @@ -169,7 +199,7 @@ const app = { this.el.settingIsCountdown.checked = true; this.el.settingIsCountdown.disabled = true; } else if (modeKey === 'custom') { - this.el.settingIsCountdown.checked = false; // Default to count up as requested + this.el.settingIsCountdown.checked = false; // Default to count up this.el.settingIsCountdown.disabled = false; } else { this.el.settingIsCountdown.checked = false; @@ -178,14 +208,14 @@ const app = { // Update List Header if (mode.hasLaps) { - this.el.listTitle.textContent = 'Laps'; + this.el.listTitle.textContent = 'Laps Recorded'; this.el.listHead.innerHTML = 'LapSplit TimeDeltaNotes'; } else if (mode.hasRelay) { this.el.listTitle.textContent = 'Relay Splits'; this.el.listHead.innerHTML = 'ParticipantTotal TimeSplitStatus'; } else if (mode.isCustom) { this.el.listTitle.textContent = 'Activities Completed'; - this.el.listHead.innerHTML = 'ActivityDurationTotal ElapsedStatus'; + this.el.listHead.innerHTML = 'ActivityDurationStatus'; } this.updateDisplay(); @@ -380,6 +410,8 @@ const app = { const delta = now - this.startTime; this.elapsedTime = delta; + this.checkAlerts(this.countdownStartValue - this.elapsedTime); + if (this.elapsedTime >= this.countdownStartValue) { this.nextActivity(); } @@ -413,14 +445,18 @@ const app = { }, checkAlerts(ms) { - if (!this.el.alertPreEnd.checked) return; + const currentAct = this.customActivities[this.currentActivityIndex]; + const isRest = currentAct && currentAct.isRest; + + const enabled = isRest ? this.el.alertRestPreEnd.checked : this.el.alertPreEnd.checked; + if (!enabled) return; const secondsLeft = Math.ceil(ms / 1000); const warningSec = parseInt(this.el.alertPreEndSec.value) || 0; if (secondsLeft <= warningSec && secondsLeft > 0 && secondsLeft !== this.lastPlayedSecond) { this.lastPlayedSecond = secondsLeft; - this.beep(523, 0.1); + this.beep(isRest ? 330 : 523, 0.1); } }, @@ -509,13 +545,14 @@ const app = { recordCustomActivity() { const act = this.customActivities[this.currentActivityIndex]; - const totalElapsedSoFar = this.history.reduce((acc, h) => acc + h.delta, 0) + act.duration; + // Skip rest activities in completed list as requested + if (act.isRest) return; + this.history.push({ id: act.name, delta: act.duration, - time: totalElapsedSoFar, - status: 'COMPLETED' + status: 'OK' }); this.renderHistory(); @@ -558,7 +595,6 @@ const app = { ${h.id} ${this.formatTime(h.delta, 'hh:mm:ss')} - ${this.formatTime(h.time, 'hh:mm:ss.ms')} ${h.status} @@ -570,31 +606,95 @@ const app = { // Custom Activity Builder renderCustomBuilder() { this.el.activityList.innerHTML = this.customActivities.map((act, i) => ` -
+
-
- - -
-
-
- - + ${act.isRest ? ` +
+
+ Rest +
+ + + + +
+
+
+ +
-
- - - - + ` : ` +
+ +
-
+
+
+ +
+ + + +
+
+
+ + + + +
+
+ `}
`).join(''); }, + onDragStart(index, event) { + this.draggedItemIndex = index; + event.dataTransfer.effectAllowed = 'move'; + // Add a small delay to make the actual element transparent while dragging + setTimeout(() => { + event.target.classList.add('dragging'); + }, 0); + }, + + onDragOver(index, event) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + }, + + onDrop(index, event) { + event.preventDefault(); + if (this.draggedItemIndex === null || this.draggedItemIndex === index) return; + + const item = this.customActivities.splice(this.draggedItemIndex, 1)[0]; + this.customActivities.splice(index, 0, item); + + this.draggedItemIndex = null; + this.renderCustomBuilder(); + }, + addActivity() { - this.customActivities.push({ name: `Activity ${this.customActivities.length + 1}`, duration: 60000 }); + this.customActivities.push({ name: `Activity ${this.customActivities.length + 1}`, duration: 60000, isRest: false }); + this.renderCustomBuilder(); + }, + + addRestAfter(index) { + const restDuration = (parseInt(this.el.settingRestDuration.value) || 30) * 1000; + this.customActivities.splice(index + 1, 0, { name: 'Rest', duration: restDuration, isRest: true }); + this.renderCustomBuilder(); + }, + + duplicateActivity(index) { + const original = this.customActivities[index]; + const clone = { ...original }; + this.customActivities.splice(index + 1, 0, clone); this.renderCustomBuilder(); }, @@ -636,6 +736,9 @@ const app = { if (!this.audioCtx) { this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } + if (this.audioCtx.state === 'suspended') { + this.audioCtx.resume(); + } const osc = this.audioCtx.createOscillator(); const gain = this.audioCtx.createGain(); diff --git a/index.php b/index.php index b75a02d..7cd13d5 100644 --- a/index.php +++ b/index.php @@ -29,16 +29,24 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; - + -