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 = '
| Lap | Split Time | Delta | Notes |
';
+ } else if (mode.hasRelay) {
+ this.el.listTitle.textContent = 'Relay Splits';
+ this.el.listHead.innerHTML = '| Participant | Total Time | Split | Status |
';
+ } else if (mode.isCustom) {
+ this.el.listTitle.textContent = 'Activities Completed';
+ this.el.listHead.innerHTML = '| Activity | Duration | Total Elapsed | Status |
';
+ }
+
+ 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
Analyzing your requirements and generating your website…
-
- Loading…
-
-
= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.
-
This page will update automatically as the plan is implemented.
-
Runtime: PHP = htmlspecialchars($phpVersion) ?> — UTC = htmlspecialchars($now) ?>
-
-
-
+
+
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+ | Item |
+ Split Time |
+ Delta |
+ Notes |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
\ No newline at end of file