Skip to content

Commit e72a3a9

Browse files
Improve time input experience (#13)
1 parent e3910dd commit e72a3a9

File tree

1 file changed

+126
-7
lines changed

1 file changed

+126
-7
lines changed

index.html

Lines changed: 126 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@
8181
transition: all 0.3s ease;
8282
}
8383

84+
.time-input.invalid {
85+
border: 2px solid #dc3545;
86+
}
87+
8488
/* Input border colors when standards are met */
8589
tr.has-value.met-a .time-input {
8690
border: 3px solid #28a745;
@@ -161,6 +165,7 @@
161165
.clear-section {
162166
text-align: center;
163167
margin-top: 2rem;
168+
margin-bottom: 2rem;
164169
padding-top: 2rem;
165170
border-top: 1px solid var(--pico-muted-border-color);
166171
}
@@ -251,29 +256,33 @@ <h4>Motivational Times Lookup</h4>
251256

252257
<!-- Clear Times Section -->
253258
<section class="clear-section">
259+
<label style="margin: 1rem auto; display: block;">
260+
<input type="checkbox" id="fastEntryToggle" role="switch" />
261+
Fast entry mode (numbers only)
262+
</label>
263+
<button type="button" id="clearTimesBtn" class="secondary">Clear all times</button>
254264
<p class="note-text" style="margin-bottom: 1rem;">
255265
💡 Tip: Rotate your phone to landscape view for better table visibility.
256266
</p>
257267
<p class="note-text" style="margin-bottom: 1rem;">
258268
Times marked with ⚠️ indicate source data issues or unexpected time progressions
259269
(<a href="https://github.com/ironprogrammer/swimcheck" target="_blank" rel="noopener noreferrer">more details on GitHub</a>).
260270
</p>
261-
<button type="button" id="clearTimesBtn" class="secondary">Clear All Times</button>
262271
</section>
263272

264273
<!-- Clear Times Confirmation Modal -->
265274
<dialog id="clearTimesModal">
266275
<article>
267276
<header>
268277
<button aria-label="Close" rel="prev" id="closeModalBtn"></button>
269-
<p><strong>Clear All Times?</strong></p>
278+
<p><strong>Clear all times</strong></p>
270279
</header>
271280
<p>
272281
Are you sure you want to clear all entered times? This cannot be undone.
273282
</p>
274283
<footer>
275284
<button type="button" class="secondary" id="cancelClearBtn">Cancel</button>
276-
<button type="button" id="confirmClearBtn">Clear All Times</button>
285+
<button type="button" id="confirmClearBtn">Clear all times</button>
277286
</footer>
278287
</article>
279288
</dialog>
@@ -301,13 +310,21 @@ <h4>Motivational Times Lookup</h4>
301310
gender: null,
302311
courseType: 'SCY',
303312
times: {},
304-
dataVersion: CURRENT_DATA_VERSION
313+
dataVersion: CURRENT_DATA_VERSION,
314+
fastEntryMode: false
305315
};
306316

307317
// Load JSON data
308318
async function loadData() {
309319
try {
310-
const response = await fetch('swim_time_standards.json');
320+
// Try local file first
321+
let response = await fetch('swim_time_standards.json');
322+
323+
// If local file doesn't exist, fall back to swimcheck.org
324+
if (!response.ok) {
325+
response = await fetch('https://swimcheck.org/swim_time_standards.json');
326+
}
327+
311328
swimData = await response.json();
312329
document.getElementById('seasonInfo').textContent = swimData.title;
313330
initializeApp();
@@ -475,6 +492,23 @@ <h4>Motivational Times Lookup</h4>
475492
}
476493
}
477494

495+
// Fast entry mode toggle
496+
const fastEntryToggle = document.getElementById('fastEntryToggle');
497+
fastEntryToggle.checked = appState.fastEntryMode;
498+
fastEntryToggle.addEventListener('change', (e) => {
499+
appState.fastEntryMode = e.target.checked;
500+
saveStateToStorage();
501+
502+
// Update inputmode on all existing time inputs
503+
document.querySelectorAll('.time-input').forEach(input => {
504+
if (appState.fastEntryMode) {
505+
input.setAttribute('inputmode', 'numeric');
506+
} else {
507+
input.removeAttribute('inputmode');
508+
}
509+
});
510+
});
511+
478512
// Clear times button - show modal
479513
document.getElementById('clearTimesBtn').addEventListener('click', () => {
480514
document.getElementById('clearTimesModal').showModal();
@@ -572,13 +606,16 @@ <h4>Motivational Times Lookup</h4>
572606
const bPlusClass = qualification === 'b-plus' ? 'standard-cell met met-b-plus' : 'standard-cell';
573607
const bClass = qualification === 'b' ? 'standard-cell met met-b' : 'standard-cell';
574608

609+
const inputModeAttr = appState.fastEntryMode ? 'inputmode="numeric"' : '';
610+
575611
tableHTML += `
576612
<tr class="${rowClass}">
577613
<td>${event.name}</td>
578614
<td>
579615
<input
580616
type="text"
581617
class="time-input"
618+
${inputModeAttr}
582619
placeholder="mm:ss.ms"
583620
data-event-key="${eventKey}"
584621
value="${userTime}"
@@ -616,7 +653,27 @@ <h4>Motivational Times Lookup</h4>
616653
inputs.forEach(input => {
617654
let timeout;
618655

656+
// Auto-select text on focus for easier replacement
657+
input.addEventListener('focus', (e) => {
658+
e.target.select();
659+
660+
// Prevent mouseup from repositioning cursor after select
661+
const preventMouseUp = (event) => {
662+
event.preventDefault();
663+
e.target.removeEventListener('mouseup', preventMouseUp);
664+
};
665+
e.target.addEventListener('mouseup', preventMouseUp, { once: true });
666+
});
667+
619668
input.addEventListener('input', (e) => {
669+
// Real-time validation feedback
670+
const value = e.target.value.trim();
671+
if (value && !isValidTimeInput(value)) {
672+
e.target.classList.add('invalid');
673+
} else {
674+
e.target.classList.remove('invalid');
675+
}
676+
620677
clearTimeout(timeout);
621678
timeout = setTimeout(() => {
622679
handleTimeInput(e.target);
@@ -630,13 +687,70 @@ <h4>Motivational Times Lookup</h4>
630687
});
631688
}
632689

690+
// Validate time input contains only valid characters
691+
function isValidTimeInput(value) {
692+
if (!value) return true;
693+
return /^[0-9:.]+$/.test(value);
694+
}
695+
696+
// Parse time input: strip non-digits, parse right-to-left to mm:ss.xx format
697+
function parseTimeInput(value) {
698+
if (!value) return '';
699+
700+
// Strip all non-digits and leading zeros
701+
let digits = value.replace(/\D/g, '');
702+
digits = digits.replace(/^0+/, '') || '0'; // Remove leading zeros, keep at least one digit
703+
if (digits.length === 0) return '';
704+
705+
const len = digits.length;
706+
707+
if (len === 1) {
708+
// Single digit: "5" → "00.05"
709+
return `00.0${digits}`;
710+
} else if (len === 2) {
711+
// Two digits: "34" → "00.34"
712+
return `00.${digits}`;
713+
} else if (len === 3) {
714+
// Three digits: "123" → "01.23"
715+
return `0${digits[0]}.${digits.slice(1)}`;
716+
} else if (len === 4) {
717+
// Four digits: "1234" → "12.34"
718+
return `${digits.slice(0, 2)}.${digits.slice(2)}`;
719+
} else {
720+
// Five or more digits: "12345" → "1:23.45", "123456" → "12:34.56"
721+
const hundredths = digits.slice(-2);
722+
const seconds = digits.slice(-4, -2);
723+
const minutes = digits.slice(0, -4);
724+
return `${minutes}:${seconds}.${hundredths}`;
725+
}
726+
}
727+
633728
// Handle time input
634729
function handleTimeInput(input) {
635730
const eventKey = input.dataset.eventKey;
636731
const value = input.value.trim();
637732

638-
if (value) {
639-
appState.times[eventKey] = value;
733+
// Check for invalid characters
734+
if (value && !isValidTimeInput(value)) {
735+
input.classList.add('invalid');
736+
delete appState.times[eventKey];
737+
saveStateToStorage();
738+
updateStandardHighlighting(input);
739+
return;
740+
}
741+
742+
// Remove invalid class if present
743+
input.classList.remove('invalid');
744+
745+
// Parse and format the time
746+
const parsedTime = parseTimeInput(value);
747+
748+
// Enforce max length of 9 characters (e.g., "190:38.98")
749+
const finalTime = parsedTime.substring(0, 9);
750+
751+
if (finalTime) {
752+
appState.times[eventKey] = finalTime;
753+
input.value = finalTime;
640754
} else {
641755
delete appState.times[eventKey];
642756
}
@@ -672,6 +786,11 @@ <h4>Motivational Times Lookup</h4>
672786
return;
673787
}
674788

789+
// Skip comparison if input is invalid
790+
if (input.classList.contains('invalid')) {
791+
return;
792+
}
793+
675794
// Find the event's times
676795
const [gender, course, eventName] = eventKey.split('_');
677796
const ageGroupData = swimData.ageGroups.find(g => g.age === appState.ageGroup);

0 commit comments

Comments
 (0)