From 9b97fdeae8f5372acdef601e00418b6c1d117e8e Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 3 Mar 2026 17:10:40 +0000 Subject: [PATCH] alpha_base --- assets/css/custom.css | 433 ++++++++++---------------- assets/js/main.js | 689 +++++++++++++++++++++++++++++++++++++++--- index.php | 401 +++++++++++++++--------- 3 files changed, 1067 insertions(+), 456 deletions(-) diff --git a/assets/css/custom.css b/assets/css/custom.css index 50e0502..1dd57f4 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,302 +1,179 @@ +:root { + --bg-color: #ffffff; + --surface-color: #f9fafb; + --text-primary: #111827; + --text-secondary: #6b7280; + --border-color: #e5e7eb; + --accent-color: #3b82f6; + --accent-hover: #2563eb; + --danger-color: #ef4444; + --success-color: #10b981; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - min-height: 100vh; + background-color: var(--bg-color); + color: var(--text-primary); + font-family: 'Inter', system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; } -.main-wrapper { - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; +.font-tabular { + font-variant-numeric: tabular-nums; + font-family: var(--font-mono); } -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); - font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 1.25rem; -} - -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { - display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.admin-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.admin-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; +/* Landing Clock */ +.hero-clock { + font-size: clamp(4rem, 15vw, 10rem); font-weight: 800; + letter-spacing: -0.05em; + line-height: 1; + color: var(--text-primary); } -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; +/* Cards & Containers */ +.card-precise { + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 4px; + transition: all 0.2s ease; } -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; +.card-precise:hover { + border-color: var(--accent-color); + background: #fff; + box-shadow: 0 4px 12px rgba(0,0,0,0.03); +} + +.btn-precise { + border-radius: 4px; + font-weight: 500; + padding: 0.5rem 1rem; + transition: all 0.1s ease; text-transform: uppercase; font-size: 0.75rem; - letter-spacing: 1px; + letter-spacing: 0.05em; } -.table td { - background: #fff; - padding: 1rem; - border: none; +/* Timer Display */ +.timer-display { + font-size: clamp(3rem, 10vw, 6rem); + font-weight: 700; + color: var(--text-primary); } -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; +.timer-sub-display { + font-size: 1.5rem; + color: var(--text-secondary); } -.form-group label { - display: block; - margin-bottom: 0.5rem; +/* Table */ +.table-precise { + font-size: 0.875rem; +} + +.table-precise th { font-weight: 600; - font-size: 0.9rem; + color: var(--text-secondary); + text-transform: uppercase; + font-size: 0.7rem; + letter-spacing: 0.05em; + border-bottom: 2px solid var(--border-color); } -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; +.badge-best { + background-color: var(--success-color); + color: white; + font-size: 0.65rem; + padding: 2px 6px; + border-radius: 2px; } -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); +/* SPA Transitions */ +.view-section { + display: none; +} + +.view-section.active { + display: block; +} + +/* Forms */ +.form-control-precise { + border-radius: 4px; + border: 1px solid var(--border-color); + font-size: 0.875rem; + padding: 0.4rem 0.75rem; +} + +.form-control-precise:focus { + border-color: var(--accent-color); + box-shadow: none; +} + +#session-title { + background-color: var(--surface-color); + border-width: 0 0 2px 0; + border-radius: 0; + padding-left: 0; +} + +#session-title:focus { + background-color: transparent; + border-color: var(--accent-color); +} + +/* Activity Builder */ +#custom-builder { + background: var(--surface-color); + padding: 1.5rem; + border: 1px dashed var(--border-color); + border-radius: 8px; +} + +.activity-row { + transition: transform 0.2s ease; + box-shadow: 0 2px 4px rgba(0,0,0,0.02); +} + +.activity-row:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.05); +} + +/* Options Dropdown */ +.dropdown-menu { + box-shadow: 0 10px 25px rgba(0,0,0,0.1) !important; + border: 1px solid var(--border-color); +} + +.dropdown-header { + color: var(--text-primary); + font-weight: 700; + letter-spacing: 0.1em; +} + +/* Utility */ +.tracking-widest { letter-spacing: 0.1em; } +.tracking-wider { letter-spacing: 0.05em; } +.fs-small { font-size: 0.75rem; } +.fs-tiny { font-size: 0.65rem; } +.cursor-pointer { cursor: pointer; } + +/* Alerts / Finishes */ +.timer-finish { + animation: flash 1s infinite; +} + +@keyframes flash { + 0% { opacity: 1; } + 50% { opacity: 0.5; } + 100% { opacity: 1; } +} + +/* Settings Switches */ +.form-check-input:checked { + background-color: var(--accent-color); + border-color: var(--accent-color); } \ No newline at end of file diff --git a/assets/js/main.js b/assets/js/main.js index d349598..ec5d31f 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,39 +1,660 @@ -document.addEventListener('DOMContentLoaded', () => { - const chatForm = document.getElementById('chat-form'); - const chatInput = document.getElementById('chat-input'); - const chatMessages = document.getElementById('chat-messages'); +/** + * INTERACTIVE TIMER APP + * Core Logic - Handles: + * - Landing Clock + * - Timer Modes (Time-watch, Countdown, Stopwatch, Lap, Relay, Custom) + * - Display Formats + * - Lap, Relay Split & Custom Activity Logic + * - Sound Alerts & Session Management + */ - const appendMessage = (text, sender) => { - const msgDiv = document.createElement('div'); - msgDiv.classList.add('message', sender); - msgDiv.textContent = text; - chatMessages.appendChild(msgDiv); - chatMessages.scrollTop = chatMessages.scrollHeight; - }; +const app = { + // STATE + currentView: 'landing', + currentMode: null, + isRunning: false, + isPaused: false, + isPreStarting: false, + startTime: 0, + elapsedTime: 0, + pausedTime: 0, + animationId: null, + history: [], // Universal storage for laps, splits, activities + countdownStartValue: 0, + + // Relay State + currentParticipant: 1, + + // Custom State + customActivities: [ + { name: 'Activity 1', duration: 60000 }, + { name: 'Activity 2', duration: 120000 } + ], + currentActivityIndex: 0, - chatForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const message = chatInput.value.trim(); - if (!message) return; + // Audio Helpers + audioCtx: null, + lastPlayedSecond: -1, - appendMessage(message, 'visitor'); - chatInput.value = ''; + // 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 }, + '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 } + }, - try { - const response = await fetch('api/chat.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ message }) - }); - const data = await response.json(); - - // Artificial delay for realism - setTimeout(() => { - appendMessage(data.reply, 'bot'); - }, 500); - } catch (error) { - console.error('Error:', error); - appendMessage("Sorry, something went wrong. Please try again.", 'bot'); + // ELEMENTS + el: { + landingClock: document.getElementById('landing-clock'), + mainTimer: document.getElementById('main-timer'), + subTimer: document.getElementById('sub-timer'), + viewLanding: document.getElementById('view-landing'), + viewTimer: document.getElementById('view-timer'), + timerTitle: document.getElementById('timer-title'), + activeActivityName: document.getElementById('active-activity-name'), + sessionTitle: document.getElementById('session-title'), + + btnStart: document.getElementById('btn-start'), + btnPause: document.getElementById('btn-pause'), + btnStop: document.getElementById('btn-stop'), + btnReset: document.getElementById('btn-reset'), + btnLap: document.getElementById('btn-lap'), + 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'), + + countdownInputs: document.getElementById('countdown-inputs'), + inputH: document.getElementById('input-h'), + inputM: document.getElementById('input-m'), + inputS: document.getElementById('input-s'), + + relayConfig: document.getElementById('relay-config'), + participantCount: document.getElementById('participant-count'), + + customBuilder: document.getElementById('custom-builder'), + activityList: document.getElementById('activity-list'), + btnAddActivity: document.getElementById('btn-add-activity'), + + // Settings / Options + optCountdownContainer: document.getElementById('opt-countdown-container'), + settingIsCountdown: document.getElementById('setting-is-countdown'), + 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') + }, + + init() { + console.log('Timer App Initializing...'); + this.updateLandingClock(); + this.bindEvents(); + this.renderCustomBuilder(); + this.resetTimer(); + }, + + bindEvents() { + this.el.btnStart.addEventListener('click', () => this.handleStartClick()); + this.el.btnPause.addEventListener('click', () => this.pauseTimer()); + this.el.btnStop.addEventListener('click', () => this.stopTimer()); + 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.brandLink.addEventListener('click', (e) => { + e.preventDefault(); + this.switchView('landing'); + }); + + this.el.formatSelect.addEventListener('change', () => this.updateDisplay()); + this.el.settingIsCountdown.addEventListener('change', () => this.resetTimer()); + }, + + updateLandingClock() { + const now = new Date(); + const h = String(now.getHours()).padStart(2, '0'); + const m = String(now.getMinutes()).padStart(2, '0'); + const s = String(now.getSeconds()).padStart(2, '0'); + + if (this.el.landingClock) { + this.el.landingClock.textContent = `${h}:${m}:${s}`; } - }); -}); + setTimeout(() => this.updateLandingClock(), 1000); + }, + + switchView(viewName) { + this.currentView = viewName; + document.querySelectorAll('.view-section').forEach(s => s.classList.remove('active')); + + if (viewName === 'landing') { + this.el.viewLanding.classList.add('active'); + this.stopTimer(); + } else { + this.el.viewTimer.classList.add('active'); + } + }, + + setMode(modeKey) { + this.currentMode = modeKey; + const mode = this.modes[modeKey]; + + this.el.timerTitle.textContent = mode.title; + this.switchView('timer'); + this.resetTimer(); + + // 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.listContainer.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.activeActivityName.classList.add('d-none'); + + // Options Box adjustments + this.el.optCountdownContainer.classList.toggle('d-none', !mode.canToggleCountdown && !mode.forceCountdown); + if (mode.forceCountdown) { + 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.disabled = false; + } else { + this.el.settingIsCountdown.checked = false; + this.el.settingIsCountdown.disabled = false; + } + + // Update List Header + if (mode.hasLaps) { + this.el.listTitle.textContent = 'Laps'; + 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.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.isRunning && !this.isPaused) return; + + // Sound Pre-start check + if (!this.isPaused && this.el.alertPreStart.checked) { + const preStartSec = parseInt(this.el.alertPreStartSec.value) || 0; + if (preStartSec > 0) { + this.runPreStartCountdown(preStartSec); + return; + } + } + + this.startTimer(); + }, + + runPreStartCountdown(seconds) { + this.isPreStarting = true; + this.el.btnStart.disabled = true; + this.el.btnReset.disabled = true; + this.el.sessionTitle.disabled = true; + + let remaining = seconds; + this.el.mainTimer.textContent = `STARTING IN ${remaining}...`; + this.beep(440, 0.1); + + const interval = setInterval(() => { + remaining--; + if (remaining > 0) { + this.el.mainTimer.textContent = `STARTING IN ${remaining}...`; + this.beep(440, 0.1); + } else { + clearInterval(interval); + this.isPreStarting = false; + this.beep(880, 0.3); + this.startTimer(); + } + }, 1000); + }, + + startTimer() { + const mode = this.modes[this.currentMode]; + const now = performance.now(); + const isCountdownMode = this.el.settingIsCountdown.checked; + + if (this.currentMode === 'countdown' && !this.isPaused) { + const h = parseInt(this.el.inputH.value) || 0; + const m = parseInt(this.el.inputM.value) || 0; + const s = parseInt(this.el.inputS.value) || 0; + const totalMs = ((h * 3600) + (m * 60) + s) * 1000; + + if (totalMs <= 0) { + alert('Please enter a duration.'); + return; + } + this.countdownStartValue = totalMs; + this.elapsedTime = totalMs; + } + + 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; + + this.countdownStartValue = this.customActivities[0].duration; + this.elapsedTime = isCountdownMode ? this.countdownStartValue : 0; + } + + if (this.isPaused) { + const offset = (isCountdownMode) ? (this.countdownStartValue - this.elapsedTime) : this.elapsedTime; + this.startTime = now - offset; + } else { + this.startTime = now; + } + + this.isRunning = true; + this.isPaused = false; + this.lastPlayedSecond = -1; + + this.el.btnStart.disabled = true; + this.el.btnPause.disabled = false; + this.el.btnStop.disabled = false; + this.el.btnReset.disabled = true; + this.el.sessionTitle.disabled = true; + this.el.mainTimer.classList.remove('timer-finish'); + + // Lock inputs + if (this.el.inputH) this.el.inputH.disabled = true; + 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; + + this.tick(); + }, + + pauseTimer() { + if (!this.isRunning || this.isPaused) return; + + this.isPaused = true; + this.pausedTime = this.elapsedTime; + cancelAnimationFrame(this.animationId); + + this.el.btnStart.disabled = false; + this.el.btnPause.disabled = true; + this.el.btnReset.disabled = false; + }, + + stopTimer() { + if (!this.isRunning && !this.isPaused) return; + + this.isRunning = false; + this.isPaused = false; + cancelAnimationFrame(this.animationId); + + this.el.btnStart.disabled = false; + this.el.btnPause.disabled = true; + this.el.btnStop.disabled = true; + this.el.btnReset.disabled = false; + + if (this.currentMode === 'lap') { + this.recordLap(); + } else if (this.currentMode === 'relay') { + this.recordRelaySplit(true); + } + }, + + resetTimer() { + this.stopTimer(); + this.elapsedTime = 0; + this.pausedTime = 0; + this.countdownStartValue = 0; + this.history = []; + this.currentParticipant = 1; + this.currentActivityIndex = 0; + this.lastPlayedSecond = -1; + + this.el.listBody.innerHTML = ''; + this.el.mainTimer.classList.remove('timer-finish'); + + this.el.btnReset.disabled = true; + this.el.btnStart.disabled = false; + this.el.btnPause.disabled = true; + this.el.btnStop.disabled = true; + this.el.activeActivityName.classList.add('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.participantCount) this.el.participantCount.disabled = false; + + this.updateDisplay(); + }, + + tick() { + if (!this.isRunning || this.isPaused) return; + + const mode = this.modes[this.currentMode]; + const now = performance.now(); + const isCountdownMode = this.el.settingIsCountdown.checked; + + if (isCountdownMode) { + const delta = now - this.startTime; + this.elapsedTime = Math.max(0, this.countdownStartValue - delta); + + this.checkAlerts(this.elapsedTime); + + if (this.elapsedTime === 0) { + if (mode.isCustom) { + this.nextActivity(); + } else { + this.updateDisplay(); + this.stopTimer(); + this.playCompletionAlert(); + return; + } + } + } else if (mode.isCustom) { + // Count up custom mode + const delta = now - this.startTime; + this.elapsedTime = delta; + + if (this.elapsedTime >= this.countdownStartValue) { + this.nextActivity(); + } + } else { + // Standard count up + this.elapsedTime = now - this.startTime; + } + + this.updateDisplay(); + this.animationId = requestAnimationFrame(() => this.tick()); + }, + + nextActivity() { + const isCountdownMode = this.el.settingIsCountdown.checked; + this.recordCustomActivity(); + this.currentActivityIndex++; + + if (this.currentActivityIndex < this.customActivities.length) { + const nextAct = this.customActivities[this.currentActivityIndex]; + this.el.activeActivityName.textContent = nextAct.name; + this.countdownStartValue = nextAct.duration; + this.elapsedTime = isCountdownMode ? this.countdownStartValue : 0; + this.startTime = performance.now(); + this.lastPlayedSecond = -1; + this.beep(660, 0.2); + } else { + this.updateDisplay(); + this.stopTimer(); + this.playCompletionAlert(); + } + }, + + checkAlerts(ms) { + if (!this.el.alertPreEnd.checked) 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); + } + }, + + updateDisplay() { + const ms = this.elapsedTime; + const format = this.el.formatSelect.value; + + this.el.mainTimer.textContent = this.formatTime(ms, format); + + if (format !== 'hh:mm:ss.ms') { + this.el.subTimer.textContent = this.formatTime(ms, 'hh:mm:ss.ms'); + this.el.subTimer.classList.remove('d-none'); + } else { + this.el.subTimer.classList.add('d-none'); + } + }, + + formatTime(ms, format) { + let absMs = Math.abs(ms); + let seconds = Math.floor(absMs / 1000); + let minutes = Math.floor(seconds / 60); + let hours = Math.floor(minutes / 60); + let milliseconds = Math.floor(absMs % 1000); + + seconds %= 60; + minutes %= 60; + + const pad = (n, l = 2) => String(n).padStart(l, '0'); + + switch (format) { + case 'hh:mm:ss.ms': + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}.${pad(milliseconds, 3)}`; + case 'hh:mm:ss': + return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; + case 'hours': + return (ms / 3600000).toFixed(4) + ' h'; + case 'minutes': + return (ms / 60000).toFixed(2) + ' m'; + case 'seconds': + return (ms / 1000).toFixed(0) + ' s'; + case 'seconds.ms': + return (ms / 1000).toFixed(3) + ' s'; + default: + return '00:00:00.000'; + } + }, + + recordLap() { + const now = this.elapsedTime; + const lastTime = this.history.length > 0 ? this.history[this.history.length - 1].time : 0; + const delta = now - lastTime; + + this.history.push({ + id: this.history.length + 1, + time: now, + delta: delta + }); + + this.renderHistory(); + }, + + recordRelaySplit(isFinal = false) { + const now = this.elapsedTime; + const lastTime = this.history.length > 0 ? this.history[this.history.length - 1].time : 0; + const delta = now - lastTime; + const maxParticipants = parseInt(this.el.participantCount.value) || 1; + + if (this.currentParticipant > maxParticipants && !isFinal) return; + + this.history.push({ + id: `P${this.currentParticipant}`, + time: now, + delta: delta, + status: isFinal ? 'DONE' : 'SPLIT' + }); + + if (!isFinal) { + this.currentParticipant++; + if (this.currentParticipant > maxParticipants) { + this.stopTimer(); + } + } + + this.renderHistory(); + }, + + recordCustomActivity() { + const act = this.customActivities[this.currentActivityIndex]; + const totalElapsedSoFar = this.history.reduce((acc, h) => acc + h.delta, 0) + act.duration; + + this.history.push({ + id: act.name, + delta: act.duration, + time: totalElapsedSoFar, + status: 'COMPLETED' + }); + + this.renderHistory(); + }, + + renderHistory() { + if (this.currentMode === 'lap') { + let minDelta = Infinity; + let bestId = -1; + this.history.forEach(h => { + if (h.delta < minDelta) { + minDelta = h.delta; + bestId = h.id; + } + }); + + 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.id === bestId ? '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(''); + } else if (this.currentMode === 'custom') { + this.el.listBody.innerHTML = this.history.slice().reverse().map(h => ` + + ${h.id} + ${this.formatTime(h.delta, 'hh:mm:ss')} + ${this.formatTime(h.time, 'hh:mm:ss.ms')} + + ${h.status} + + + `).join(''); + } + }, + + // Custom Activity Builder + renderCustomBuilder() { + this.el.activityList.innerHTML = this.customActivities.map((act, i) => ` +
+
+
+ + +
+
+
+ + +
+
+ + + + +
+
+
+
+ `).join(''); + }, + + addActivity() { + this.customActivities.push({ name: `Activity ${this.customActivities.length + 1}`, duration: 60000 }); + this.renderCustomBuilder(); + }, + + removeActivity(index) { + if (this.customActivities.length <= 1) return; + this.customActivities.splice(index, 1); + this.renderCustomBuilder(); + }, + + updateActName(index, val) { + this.customActivities[index].name = val; + }, + + applyUnit(index, unit) { + const input = document.getElementById(`act-input-${index}`); + const val = parseFloat(input.value) || 0; + let ms = 0; + + switch(unit) { + case 'H': ms = val * 3600000; break; + case 'M': ms = val * 60000; break; + case 'S': ms = val * 1000; break; + } + + this.customActivities[index].duration = ms; + this.renderCustomBuilder(); + }, + + playCompletionAlert() { + this.el.mainTimer.classList.add('timer-finish'); + if (this.el.alertCompletion.checked) { + this.beep(880, 0.5); + setTimeout(() => this.beep(1100, 0.5), 200); + } + }, + + beep(freq = 440, duration = 0.1) { + try { + if (!this.audioCtx) { + this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + const osc = this.audioCtx.createOscillator(); + const gain = this.audioCtx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, this.audioCtx.currentTime); + + gain.gain.setValueAtTime(0.1, this.audioCtx.currentTime); + gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + duration); + + osc.connect(gain); + gain.connect(this.audioCtx.destination); + + osc.start(); + osc.stop(this.audioCtx.currentTime + duration); + } catch (e) { + console.warn('Audio error:', e); + } + } +}; + +// Start the app +document.addEventListener('DOMContentLoaded', () => app.init()); \ No newline at end of file diff --git a/index.php b/index.php index 7205f3d..b75a02d 100644 --- a/index.php +++ b/index.php @@ -1,150 +1,263 @@ - + - - - New Style - - - - - - - - - - - - - - - - - - - + + + <?php echo htmlspecialchars($projectName); ?> + + + + + + + + + + + + + + + + + + - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
-
- + + + + + +
+ + +
+
+

Current Local Time

+

00:00:00

+

+ Select a timer mode below to begin. Modern, precise timing for any activity. +

+
+ +
+
+
+

Time-watch

+

The basic timer model. Start, pause, or stop at any moment.

+ +
+
+
+
+

Countdown

+

Enter a duration and watch it count down to zero.

+ +
+
+
+
+

Stopwatch

+

Precision timing without pause functionality.

+ +
+
+
+
+

Lap Timer

+

Log splits and find your best lap performance.

+ +
+
+
+
+

Relay Timer

+

Participant split tracking for multi-stage timing.

+ +
+
+
+
+

Custom Timer

+

Create multiple chained activities.

+ +
+
+
+
+ + +
+
+ + +
+
+

Timer

+
Activity Name
+
+
+ + + +
+ + +
+
+
+ + +
+ + +
+ +
00:00:00.000
+
00:00:00
+ + +
+ + + + + + +
+ + +
+
+ +
+ + : + + : + +
+
+ +
+ + +
+
+ + +
+
+

Activities

+ +
+
+ +
+
+ +
+ + +
+

History

+
+ + + + + + + + + + + + +
ItemSplit TimeDeltaNotes
+
+
+
+ +
+ + + + + + + - + \ No newline at end of file