diff --git a/api/timers.php b/api/timers.php new file mode 100644 index 0000000..c6a7cc9 --- /dev/null +++ b/api/timers.php @@ -0,0 +1,41 @@ +query("SELECT * FROM saved_timers ORDER BY created_at DESC"); + $timers = $stmt->fetchAll(); + echo json_encode(['success' => true, 'data' => $timers]); + } + elseif ($method === 'POST') { + $data = json_decode(file_get_contents('php://input'), true); + if (empty($data['name']) || empty($data['config'])) { + throw new Exception("Missing name or config"); + } + + $stmt = $pdo->prepare("INSERT INTO saved_timers (name, config) VALUES (:name, :config)"); + $stmt->execute([ + ':name' => $data['name'], + ':config' => json_encode($data['config']) + ]); + echo json_encode(['success' => true, 'id' => $pdo->lastInsertId()]); + } + elseif ($method === 'DELETE') { + $id = $_GET['id'] ?? null; + if (!$id) { + throw new Exception("Missing id"); + } + + $stmt = $pdo->prepare("DELETE FROM saved_timers WHERE id = :id"); + $stmt->execute([':id' => $id]); + echo json_encode(['success' => true]); + } +} catch (Exception $e) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => $e->getMessage()]); +} diff --git a/assets/css/custom.css b/assets/css/custom.css index 119a287..afeabe1 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -168,6 +168,16 @@ body { padding: 0.4rem 0.75rem; } +.form-control-compact { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; +} + +.btn-compact { + padding: 0.2rem 0.4rem; + font-size: 0.65rem; +} + #session-title { background-color: transparent; border-width: 0 0 2px 0; @@ -218,19 +228,6 @@ body { 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 { background-color: var(--surface-color); @@ -261,6 +258,7 @@ body { /* Alerts / Finishes */ .timer-finish { + color: var(--danger-color) !important; animation: flash 1s infinite; } @@ -270,6 +268,16 @@ body { 100% { opacity: 1; } } +@keyframes blink-red { + 0% { color: inherit; } + 50% { color: var(--danger-color); } + 100% { color: inherit; } +} + +.timer-alert { + animation: blink-red 0.5s infinite; +} + /* Settings Switches */ .form-check-input:checked { background-color: var(--accent-color); @@ -280,19 +288,19 @@ body { .timer-workspace-container { display: flex; flex-wrap: wrap; - gap: 2rem; + gap: 3rem; justify-content: center; + align-items: flex-start; } .timer-main-column { - flex: 1; + flex: 0 1 600px; min-width: 320px; - max-width: 800px; order: 2; /* Main on right on desktop */ } .timer-side-column { - width: 380px; + width: 450px; order: 1; /* History on left on desktop */ } @@ -304,6 +312,8 @@ body { order: 2; /* Move history back to bottom on mobile */ } .timer-main-column { + flex: 1; + max-width: 800px; order: 1; /* Main timer on top on mobile */ } } @@ -342,4 +352,9 @@ body.dark-mode .font-tabular { } body.dark-mode .text-muted { color: var(--text-secondary) !important; +} + +/* Saved Timers Hover Effect */ +#saved-timers-list .card-precise:hover { + border-color: var(--success-color); } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index 3fcfe53..28304da 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -29,11 +29,19 @@ const app = { // Custom State customActivities: [ - { name: 'Activity 1', duration: 60000, isRest: false }, - { name: 'Rest', duration: 30000, isRest: true } + { name: 'Activity 1', duration: 60000, isRest: false } ], currentActivityIndex: 0, - draggedItemIndex: null, + + // Session titles stored per mode + sessionTitles: { + 'time-watch': '', + 'countdown': '', + 'stopwatch': '', + 'lap': '', + 'relay': '', + 'custom': '' + }, // Audio Helpers audioCtx: null, @@ -42,11 +50,11 @@ const app = { // CONFIG modes: { 'time-watch': { title: 'Time-watch', allowPause: true, hasLaps: false, canToggleCountdown: true }, - 'countdown': { title: 'Countdown Timer', allowPause: true, hasLaps: false, forceCountdown: true }, - 'stopwatch': { title: 'Stopwatch', allowPause: false, hasLaps: false, canToggleCountdown: false }, + '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 }, - 'custom': { title: 'Custom Timer', allowPause: true, isCustom: true, canToggleCountdown: true } + 'custom': { title: 'Custom Timer', allowPause: true, isCustom: true, canToggleCountdown: true, canSave: true } }, // ELEMENTS @@ -68,11 +76,15 @@ const app = { btnReset: document.getElementById('btn-reset'), btnLap: document.getElementById('btn-lap'), btnNext: document.getElementById('btn-next'), + btnSaveMain: document.getElementById('btn-save-main'), + btnSaveBuilder: document.getElementById('btn-save-session-builder'), formatSelect: document.getElementById('format-select'), listTitle: document.getElementById('list-title'), listHead: document.getElementById('list-head'), listBody: document.getElementById('list-body'), + lapSort: document.getElementById('lap-sort'), + lapResultsContainer: document.getElementById('lap-results-container'), countdownInputs: document.getElementById('countdown-inputs'), inputH: document.getElementById('input-h'), @@ -84,7 +96,10 @@ const app = { customBuilder: document.getElementById('custom-builder'), activityList: document.getElementById('activity-list'), - btnAddActivity: document.getElementById('btn-add-activity'), + customTotalDuration: document.getElementById('custom-total-duration'), + savedTimersContainer: document.getElementById('saved-timers-container'), + savedTimersDropdown: document.getElementById('saved-timers-dropdown'), + btnDeleteSaved: document.getElementById('btn-delete-saved'), // Settings / Options optCountdownContainer: document.getElementById('opt-countdown-container'), @@ -111,6 +126,7 @@ const app = { this.bindEvents(); this.renderCustomBuilder(); this.resetTimer(); + this.loadSavedTimers(); }, initDarkMode() { @@ -137,7 +153,8 @@ 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.btnAddActivity.addEventListener('click', () => this.addActivity()); + this.el.btnSaveMain.addEventListener('click', () => this.saveCurrentTimer()); + this.el.btnSaveBuilder.addEventListener('click', () => this.saveCurrentTimer()); this.el.brandLink.addEventListener('click', (e) => { e.preventDefault(); @@ -147,6 +164,28 @@ const app = { this.el.darkModeToggle.addEventListener('click', () => this.toggleDarkMode()); this.el.formatSelect.addEventListener('change', () => this.updateDisplay()); this.el.settingIsCountdown.addEventListener('change', () => this.resetTimer()); + this.el.lapSort.addEventListener('change', () => this.renderHistory()); + + this.el.savedTimersDropdown.addEventListener('change', (e) => { + if (e.target.value) { + this.loadTimer(e.target.value); + } + }); + + this.el.btnDeleteSaved.addEventListener('click', () => { + const id = this.el.savedTimersDropdown.value; + if (id) { + this.deleteTimer(id); + } else { + alert('Please select a session to delete.'); + } + }); + + this.el.sessionTitle.addEventListener('input', (e) => { + if (this.currentMode) { + this.sessionTitles[this.currentMode] = e.target.value; + } + }); }, updateLandingClock() { @@ -181,19 +220,25 @@ const app = { this.switchView('timer'); this.resetTimer(); + // Update session title for the specific mode + this.el.sessionTitle.value = this.sessionTitles[modeKey] || ''; + // 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.btnSaveMain.classList.toggle('d-none', !mode.canSave || mode.isCustom); this.el.timerSideColumn.classList.toggle('d-none', !mode.hasLaps && !mode.hasRelay && !mode.isCustom); if (mode.isCustom) { this.el.customBuilder.classList.remove('d-none'); this.el.historySection.classList.add('d-none'); + this.el.savedTimersContainer.classList.remove('d-none'); } else { this.el.customBuilder.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'); @@ -201,6 +246,7 @@ const app = { 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.lapSort.classList.add('d-none'); // Options Box adjustments this.el.optCountdownContainer.classList.toggle('d-none', !mode.canToggleCountdown && !mode.forceCountdown); @@ -223,18 +269,28 @@ const app = { this.el.listTitle.textContent = 'Relay Splits'; this.el.listHead.innerHTML = 'ParticipantTotal TimeSplitStatus'; } else if (mode.isCustom) { - this.el.listTitle.textContent = 'Activities Completed'; + this.el.listTitle.textContent = 'Activities'; this.el.listHead.innerHTML = 'ActivityDurationStatus'; } + this.loadSavedTimers(); this.updateDisplay(); }, handleStartClick() { if (!this.el.sessionTitle.value.trim()) { - alert('Please enter a Session Name or Title before starting.'); - this.el.sessionTitle.focus(); - return; + if (this.currentMode === 'custom') { + alert('Please enter a Session Name or Title before starting.'); + this.el.sessionTitle.focus(); + return; + } else { + // Default to current day and time + const now = new Date(); + this.el.sessionTitle.value = now.toLocaleString(); + if (this.currentMode) { + this.sessionTitles[this.currentMode] = this.el.sessionTitle.value; + } + } } if (this.isRunning && !this.isPaused) return; @@ -297,7 +353,7 @@ const app = { if (mode.isCustom && !this.isPaused) { this.currentActivityIndex = 0; this.history = []; - this.renderHistory(); + this.el.activeActivityName.classList.remove('d-none'); this.el.activeActivityName.textContent = this.customActivities[0].name; @@ -307,6 +363,12 @@ const app = { // Transition: Builder -> History this.el.customBuilder.classList.add('d-none'); this.el.historySection.classList.remove('d-none'); + this.el.savedTimersContainer.classList.add('d-none'); + + // Add first activity as "in-progress" + this.updateCurrentActivityStatus('in-progress'); + } else if (mode.isCustom && this.isPaused) { + this.updateCurrentActivityStatus('in-progress'); } if (this.isPaused) { @@ -326,6 +388,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-alert'); // Lock inputs if (this.el.inputH) this.el.inputH.disabled = true; @@ -346,11 +409,16 @@ const app = { this.el.btnStart.disabled = false; this.el.btnPause.disabled = true; this.el.btnReset.disabled = false; + + if (this.currentMode === 'custom') { + this.updateCurrentActivityStatus('paused'); + } }, stopTimer() { if (!this.isRunning && !this.isPaused) return; + const wasRunning = this.isRunning; this.isRunning = false; this.isPaused = false; cancelAnimationFrame(this.animationId); @@ -359,11 +427,18 @@ const app = { this.el.btnPause.disabled = true; this.el.btnStop.disabled = true; this.el.btnReset.disabled = false; + this.el.mainTimer.classList.remove('timer-alert'); if (this.currentMode === 'lap') { this.recordLap(); + // Place list under timer + this.el.lapResultsContainer.classList.remove('d-none'); + this.el.lapResultsContainer.appendChild(this.el.historySection); + this.el.lapSort.classList.remove('d-none'); } else if (this.currentMode === 'relay') { this.recordRelaySplit(true); + } else if (this.currentMode === 'custom' && wasRunning) { + this.updateCurrentActivityStatus('Canceled'); } }, @@ -379,6 +454,7 @@ const app = { this.el.listBody.innerHTML = ''; this.el.mainTimer.classList.remove('timer-finish'); + this.el.mainTimer.classList.remove('timer-alert'); this.el.btnReset.disabled = true; this.el.btnStart.disabled = false; @@ -396,6 +472,16 @@ const app = { 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'); } this.updateDisplay(); @@ -445,7 +531,10 @@ const app = { nextActivity() { const isCountdownMode = this.el.settingIsCountdown.checked; - this.recordCustomActivity(); + + // Mark current as OK + this.updateCurrentActivityStatus('OK'); + this.currentActivityIndex++; if (this.currentActivityIndex < this.customActivities.length) { @@ -456,6 +545,8 @@ const app = { this.startTime = performance.now(); this.lastPlayedSecond = -1; this.beep(660, 0.2); + + this.updateCurrentActivityStatus('in-progress'); } else { this.updateDisplay(); this.stopTimer(); @@ -463,19 +554,46 @@ const app = { } }, + updateCurrentActivityStatus(status) { + const act = this.customActivities[this.currentActivityIndex]; + if (!act) return; + + const existingIdx = this.history.findIndex(h => h.index === this.currentActivityIndex); + if (existingIdx !== -1) { + this.history[existingIdx].status = status; + } else { + this.history.push({ + index: this.currentActivityIndex, + id: act.name, + delta: act.duration, + isRest: act.isRest, + status: status + }); + } + this.renderHistory(); + }, + checkAlerts(ms) { 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; + if (!enabled) { + this.el.mainTimer.classList.remove('timer-alert'); + 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(isRest ? 330 : 523, 0.1); + if (secondsLeft <= warningSec && secondsLeft > 0) { + this.el.mainTimer.classList.add('timer-alert'); + if (secondsLeft !== this.lastPlayedSecond) { + this.lastPlayedSecond = secondsLeft; + this.beep(isRest ? 330 : 523, 0.1); + } + } else { + this.el.mainTimer.classList.remove('timer-alert'); } }, @@ -485,10 +603,11 @@ const app = { this.el.mainTimer.textContent = this.formatTime(ms, format); + // Only show sub-timer if format is not milliseconds as requested if (format !== 'hh:mm:ss.ms') { - this.el.subTimer.textContent = this.formatTime(ms, 'hh:mm:ss.ms'); - this.el.subTimer.classList.remove('d-none'); + this.el.subTimer.classList.add('d-none'); } else { + // If main format IS milliseconds, we don't need a sub-timer showing the same thing this.el.subTimer.classList.add('d-none'); } }, @@ -562,39 +681,31 @@ const app = { this.renderHistory(); }, - recordCustomActivity() { - const act = this.customActivities[this.currentActivityIndex]; - - // Skip rest activities in completed list as requested - if (act.isRest) return; - - this.history.push({ - id: act.name, - delta: act.duration, - status: 'OK' - }); - - this.renderHistory(); - }, - renderHistory() { if (this.currentMode === 'lap') { + let sortedHistory = this.history.slice(); + const sortVal = this.el.lapSort.value; + + if (sortVal === 'best') { + sortedHistory.sort((a, b) => a.delta - b.delta); + } else if (sortVal === 'worse') { + sortedHistory.sort((a, b) => b.delta - a.delta); + } else { + sortedHistory.reverse(); + } + let minDelta = Infinity; - let bestId = -1; this.history.forEach(h => { - if (h.delta < minDelta) { - minDelta = h.delta; - bestId = h.id; - } + if (h.delta < minDelta) minDelta = h.delta; }); - this.el.listBody.innerHTML = this.history.slice().reverse().map(h => ` - + this.el.listBody.innerHTML = sortedHistory.map(h => ` + #${h.id} ${this.formatTime(h.time, 'hh:mm:ss.ms')} +${this.formatTime(h.delta, 'seconds.ms')} - ${h.id === bestId ? 'BEST' : ''} + ${h.delta === minDelta ? 'BEST' : ''} `).join(''); @@ -610,97 +721,82 @@ const app = { `).join(''); } else if (this.currentMode === 'custom') { - this.el.listBody.innerHTML = this.history.slice().reverse().map(h => ` - - ${h.id} - ${this.formatTime(h.delta, 'hh:mm:ss')} - - ${h.status} - - - `).join(''); + this.el.listBody.innerHTML = this.history.slice().reverse().map(h => { + let badgeClass = 'bg-success'; + if (h.status === 'in-progress') badgeClass = 'bg-primary'; + if (h.status === 'paused') badgeClass = 'bg-warning text-dark'; + if (h.status === 'Canceled') badgeClass = 'bg-danger'; + + return ` + + ${h.id} + ${this.formatTime(h.delta, 'hh:mm:ss')} + + ${h.status} + + + `; + }).join(''); } }, // Custom Activity Builder renderCustomBuilder() { - this.el.activityList.innerHTML = this.customActivities.map((act, i) => ` -
+ let totalMs = 0; + this.el.activityList.innerHTML = this.customActivities.map((act, i) => { + totalMs += act.duration; + const isFirst = i === 0; + return ` +
${act.isRest ? `
-
- Rest -
-
- - - - -
-
- -
+
+ 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; + `}).join(''); - const item = this.customActivities.splice(this.draggedItemIndex, 1)[0]; - this.customActivities.splice(index, 0, item); - - this.draggedItemIndex = null; - this.renderCustomBuilder(); + if (this.el.customTotalDuration) { + this.el.customTotalDuration.textContent = `Total: ${this.formatTime(totalMs, 'hh:mm:ss')}`; + } }, addActivity() { @@ -708,8 +804,13 @@ const app = { this.renderCustomBuilder(); }, + addActivityAfter(index) { + this.customActivities.splice(index + 1, 0, { name: `Activity ${this.customActivities.length + 1}`, duration: 60000, isRest: false }); + this.renderCustomBuilder(); + }, + addRestAfter(index) { - const restDuration = (parseInt(this.el.settingRestDuration.value) || 30) * 1000; + const restDuration = (parseInt(this.el.settingRestDuration.value) || 5) * 1000; this.customActivities.splice(index + 1, 0, { name: 'Rest', duration: restDuration, isRest: true }); this.renderCustomBuilder(); }, @@ -746,6 +847,155 @@ const app = { this.renderCustomBuilder(); }, + // Saved Timers Logic + async saveCurrentTimer() { + const name = this.el.sessionTitle.value.trim(); + if (!name) { + alert('Please enter a session name to save.'); + this.el.sessionTitle.focus(); + return; + } + + const config = { + mode: this.currentMode, + isCountdown: this.el.settingIsCountdown.checked, + format: this.el.formatSelect.value + }; + + if (this.currentMode === 'custom') { + config.activities = this.customActivities; + } else if (this.currentMode === 'countdown') { + config.duration = { + h: this.el.inputH.value, + m: this.el.inputM.value, + s: this.el.inputS.value + }; + } + + try { + const resp = await fetch('api/timers.php', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, config }) + }); + const result = await resp.json(); + if (result.success) { + this.loadSavedTimers(); + 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(); + } + } else { + alert('Error saving session: ' + result.error); + } + } catch (e) { + console.error(e); + alert('Failed to connect to server.'); + } + }, + + async loadSavedTimers() { + try { + const resp = await fetch('api/timers.php'); + const result = await resp.json(); + if (result.success) { + this.renderSavedTimers(result.data); + } + } catch (e) { + console.error('Failed to load saved timers:', e); + } + }, + + renderSavedTimers(timers) { + if (!timers || timers.length === 0) { + this.el.savedTimersDropdown.innerHTML = ''; + return; + } + + let html = ''; + timers.forEach(t => { + const config = JSON.parse(t.config); + // Show custom timers even if they don't have a mode yet (backwards compat) + if (config.mode === this.currentMode || (this.currentMode === 'custom' && !config.mode)) { + const durationStr = this.getDurationStringFromConfig(config); + const displayLabel = durationStr ? `${t.name} (${durationStr})` : t.name; + html += ``; + } + }); + this.el.savedTimersDropdown.innerHTML = html; + }, + + getDurationStringFromConfig(config) { + let totalMs = 0; + if (config.mode === 'custom' || (!config.mode && this.currentMode === 'custom')) { + if (config.activities) { + config.activities.forEach(a => totalMs += (a.duration || 0)); + } + } else if (config.mode === 'countdown' && config.duration) { + const h = parseInt(config.duration.h) || 0; + const m = parseInt(config.duration.m) || 0; + const s = parseInt(config.duration.s) || 0; + totalMs = ((h * 3600) + (m * 60) + s) * 1000; + } else { + return null; + } + + if (totalMs <= 0) return null; + return this.formatTime(totalMs, 'hh:mm:ss'); + }, + + async loadTimer(id) { + try { + const resp = await fetch('api/timers.php'); + const result = await resp.json(); + if (result.success) { + const timer = result.data.find(t => t.id == id); + if (timer) { + const config = JSON.parse(timer.config); + this.el.sessionTitle.value = timer.name; + if (this.currentMode) this.sessionTitles[this.currentMode] = timer.name; + this.el.settingIsCountdown.checked = !!config.isCountdown; + this.el.formatSelect.value = config.format || 'hh:mm:ss.ms'; + + if (config.mode === 'custom' || (!config.mode && this.currentMode === 'custom')) { + this.customActivities = config.activities || []; + this.renderCustomBuilder(); + } else if (config.mode === 'countdown' && config.duration) { + this.el.inputH.value = config.duration.h || ''; + this.el.inputM.value = config.duration.m || ''; + this.el.inputS.value = config.duration.s || ''; + } + + this.resetTimer(); + } + } + } catch (e) { + console.error(e); + } + }, + + async deleteTimer(id) { + if (!confirm('Are you sure you want to delete this saved session?')) return; + + try { + const resp = await fetch(`api/timers.php?id=${id}`, { method: 'DELETE' }); + const result = await resp.json(); + if (result.success) { + this.loadSavedTimers(); + alert('Session deleted.'); + } else { + alert('Error deleting: ' + result.error); + } + } catch (e) { + console.error(e); + } + }, + playCompletionAlert() { this.el.mainTimer.classList.add('timer-finish'); if (this.el.alertCompletion.checked) { diff --git a/db/migrations/20260303_create_saved_timers.sql b/db/migrations/20260303_create_saved_timers.sql new file mode 100644 index 0000000..bb8647f --- /dev/null +++ b/db/migrations/20260303_create_saved_timers.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS saved_timers ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + config TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/index.php b/index.php index d8a2b90..20e2b15 100644 --- a/index.php +++ b/index.php @@ -111,6 +111,91 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+ + +
+
+

Timer

+
+
+ + +
+
@@ -120,8 +205,15 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-

Activities Completed

-
+
+

Activities Completed

+ +
+
@@ -140,10 +232,16 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-

Activities Chain

- +
+

Activities

+ +
+
+ +
-
+ +
@@ -154,116 +252,14 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
- -
-
-

Timer

-
-
- - -
-
-
-
Activity Name
-
- -
00:00:00.000
-
00:00:00
- - -
- - - - - - -
- - -
-
+ +
-
+
: @@ -272,6 +268,40 @@ $projectImage = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
+
Activity Name
+
+ +
00:00:00.000
+
00:00:00
+ + +
+ + + + + + + +
+ + +
+
+ + +
+

Saved Sessions

+
+ + +
+
+ + +