chore(frontend): add time braker
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
David Fencl
2025-11-26 21:13:10 +01:00
parent 62f1d9e6d8
commit 188c7d4bc6
10 changed files with 969 additions and 82 deletions

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Work Break Timer</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1 class="title">Work Break Timer</h1>
<div class="timer-display" id="timerDisplay">22:00</div>
<div class="controls-section">
<div class="adjustment-buttons">
<button class="btn adjust-btn" data-minutes="1">+1 min</button>
<button class="btn adjust-btn" data-minutes="5">+5 min</button>
<button class="btn adjust-btn" data-minutes="10">+10 min</button>
<button class="btn adjust-btn" data-minutes="30">+30 min</button>
</div>
<div class="main-controls">
<button class="btn control-btn start-btn" id="startBtn">Start</button>
<button class="btn control-btn reset-btn" id="resetBtn">Reset</button>
</div>
</div>
</div>
<!-- Simple beep sound using data URI to avoid external dependencies if possible,
but for better browser compatibility across strict policies, we might need user interaction first.
The requirements said "simple HTML5 audio element or generated beep".
I'll use a generated beep in JS or a simple oscillator, but for the HTML structure,
I'll leave a placeholder or just handle it in JS.
Let's stick to handling it in JS with AudioContext for a generated beep as it's cleaner than a file.
But the plan mentioned an audio element. I'll add a hidden one just in case I want to use a file later,
but I'll primarily use JS for the beep as it's "pure" and doesn't require assets. -->
<script src="main.js"></script>
</body>
</html>

149
work-break-timer/main.js Normal file
View File

@@ -0,0 +1,149 @@
/**
* Work Break Timer Logic
*/
// Constants
const DEFAULT_TIME_MINUTES = 22;
const MAX_TIME_MINUTES = 180;
const SECONDS_PER_MINUTE = 60;
// State
let timeLeft = DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE; // in seconds
let isRunning = false;
let intervalId = null;
let audioContext = null;
// DOM Elements
const timerDisplay = document.getElementById('timerDisplay');
const startBtn = document.getElementById('startBtn');
const resetBtn = document.getElementById('resetBtn');
const adjustButtons = document.querySelectorAll('.adjust-btn');
/**
* Formats seconds into MM:SS string
*/
function formatTime(seconds) {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
/**
* Updates the timer display
*/
function updateDisplay() {
timerDisplay.textContent = formatTime(timeLeft);
// Update document title
document.title = `${formatTime(timeLeft)} - Work Timer`;
}
/**
* Plays a simple beep sound using AudioContext
*/
function playAlarm() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
// Create oscillator
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioContext.currentTime); // A5
oscillator.frequency.exponentialRampToValueAtTime(440, audioContext.currentTime + 0.5); // Drop to A4
gainNode.gain.setValueAtTime(0.5, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.5);
}
/**
* Handles the timer tick
*/
function onTick() {
if (timeLeft > 0) {
timeLeft--;
updateDisplay();
}
else {
// Time is up
pauseTimer();
timerDisplay.classList.add('time-up');
playAlarm();
// Play alarm 3 times
setTimeout(playAlarm, 1000);
setTimeout(playAlarm, 2000);
}
}
/**
* Starts or resumes the timer
*/
function startTimer() {
if (timeLeft === 0) {
// If time is 0, reset to default before starting?
// Or just do nothing? Requirement says: "Start from currently set time".
// If currently set is 0, we can't really start.
// Let's assume if 0, we reset to default for convenience, or just return.
// Let's just return to be safe, or maybe the user wants to add time first.
if (timeLeft === 0)
return;
}
if (!isRunning) {
isRunning = true;
startBtn.textContent = 'Pause';
timerDisplay.classList.remove('time-up');
// Use window.setInterval to avoid TypeScript confusion with NodeJS.Timeout
intervalId = window.setInterval(onTick, 1000);
}
else {
// Pause
pauseTimer();
}
}
/**
* Pauses the timer
*/
function pauseTimer() {
isRunning = false;
startBtn.textContent = 'Resume';
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
}
/**
* Resets the timer
*/
function resetTimer() {
pauseTimer();
timeLeft = DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE;
startBtn.textContent = 'Start';
timerDisplay.classList.remove('time-up');
updateDisplay();
document.title = 'Work Break Timer';
}
/**
* Adjusts the time by adding minutes
*/
function adjustTime(minutes) {
const newTime = timeLeft + (minutes * SECONDS_PER_MINUTE);
const maxSeconds = MAX_TIME_MINUTES * SECONDS_PER_MINUTE;
if (newTime <= maxSeconds) {
timeLeft = newTime;
updateDisplay();
// If timer finished and we add time, remove the alarm style
if (timeLeft > 0) {
timerDisplay.classList.remove('time-up');
}
}
else {
// Cap at max
timeLeft = maxSeconds;
updateDisplay();
}
}
// Event Listeners
startBtn.addEventListener('click', startTimer);
resetBtn.addEventListener('click', resetTimer);
adjustButtons.forEach(btn => {
btn.addEventListener('click', () => {
const minutes = parseInt(btn.getAttribute('data-minutes') || '0', 10);
adjustTime(minutes);
});
});
// Initialize
updateDisplay();

169
work-break-timer/main.ts Normal file
View File

@@ -0,0 +1,169 @@
/**
* Work Break Timer Logic
*/
// Constants
const DEFAULT_TIME_MINUTES = 22;
const MAX_TIME_MINUTES = 180;
const SECONDS_PER_MINUTE = 60;
// State
let timeLeft = DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE; // in seconds
let isRunning = false;
let intervalId: number | null = null;
let audioContext: AudioContext | null = null;
// DOM Elements
const timerDisplay = document.getElementById('timerDisplay') as HTMLElement;
const startBtn = document.getElementById('startBtn') as HTMLButtonElement;
const resetBtn = document.getElementById('resetBtn') as HTMLButtonElement;
const adjustButtons = document.querySelectorAll('.adjust-btn');
/**
* Formats seconds into MM:SS string
*/
function formatTime(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
/**
* Updates the timer display
*/
function updateDisplay(): void {
timerDisplay.textContent = formatTime(timeLeft);
// Update document title
document.title = `${formatTime(timeLeft)} - Work Timer`;
}
/**
* Plays a simple beep sound using AudioContext
*/
function playAlarm(): void {
if (!audioContext) {
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
}
// Create oscillator
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, audioContext.currentTime); // A5
oscillator.frequency.exponentialRampToValueAtTime(440, audioContext.currentTime + 0.5); // Drop to A4
gainNode.gain.setValueAtTime(0.5, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
oscillator.stop(audioContext.currentTime + 0.5);
}
/**
* Handles the timer tick
*/
function onTick(): void {
if (timeLeft > 0) {
timeLeft--;
updateDisplay();
} else {
// Time is up
pauseTimer();
timerDisplay.classList.add('time-up');
playAlarm();
// Play alarm 3 times
setTimeout(playAlarm, 1000);
setTimeout(playAlarm, 2000);
}
}
/**
* Starts or resumes the timer
*/
function startTimer(): void {
if (timeLeft === 0) {
// If time is 0, reset to default before starting?
// Or just do nothing? Requirement says: "Start from currently set time".
// If currently set is 0, we can't really start.
// Let's assume if 0, we reset to default for convenience, or just return.
// Let's just return to be safe, or maybe the user wants to add time first.
if (timeLeft === 0) return;
}
if (!isRunning) {
isRunning = true;
startBtn.textContent = 'Pause';
timerDisplay.classList.remove('time-up');
// Use window.setInterval to avoid TypeScript confusion with NodeJS.Timeout
intervalId = window.setInterval(onTick, 1000);
} else {
// Pause
pauseTimer();
}
}
/**
* Pauses the timer
*/
function pauseTimer(): void {
isRunning = false;
startBtn.textContent = 'Resume';
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
}
/**
* Resets the timer
*/
function resetTimer(): void {
pauseTimer();
timeLeft = DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE;
startBtn.textContent = 'Start';
timerDisplay.classList.remove('time-up');
updateDisplay();
document.title = 'Work Break Timer';
}
/**
* Adjusts the time by adding minutes
*/
function adjustTime(minutes: number): void {
const newTime = timeLeft + (minutes * SECONDS_PER_MINUTE);
const maxSeconds = MAX_TIME_MINUTES * SECONDS_PER_MINUTE;
if (newTime <= maxSeconds) {
timeLeft = newTime;
updateDisplay();
// If timer finished and we add time, remove the alarm style
if (timeLeft > 0) {
timerDisplay.classList.remove('time-up');
}
} else {
// Cap at max
timeLeft = maxSeconds;
updateDisplay();
}
}
// Event Listeners
startBtn.addEventListener('click', startTimer);
resetBtn.addEventListener('click', resetTimer);
adjustButtons.forEach(btn => {
btn.addEventListener('click', () => {
const minutes = parseInt(btn.getAttribute('data-minutes') || '0', 10);
adjustTime(minutes);
});
});
// Initialize
updateDisplay();

144
work-break-timer/styles.css Normal file
View File

@@ -0,0 +1,144 @@
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--primary-color: #2196F3;
--primary-hover: #1976D2;
--secondary-color: #757575;
--secondary-hover: #616161;
--danger-color: #f44336;
--timer-bg: #fff;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: var(--timer-bg);
padding: 2rem;
border-radius: 12px;
box-shadow: var(--shadow);
text-align: center;
width: 90%;
max-width: 400px;
}
.title {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: var(--text-color);
}
.timer-display {
font-size: 4rem;
font-weight: bold;
font-variant-numeric: tabular-nums;
margin-bottom: 2rem;
padding: 1rem;
border-radius: 8px;
background-color: #fafafa;
transition: background-color 0.3s ease, color 0.3s ease;
}
.timer-display.time-up {
background-color: var(--danger-color);
color: white;
animation: flash 1s infinite;
}
@keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.controls-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.adjustment-buttons {
display: flex;
justify-content: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.btn {
border: none;
border-radius: 6px;
padding: 0.75rem 1rem;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s, transform 0.1s;
font-weight: 500;
}
.btn:active {
transform: scale(0.98);
}
.adjust-btn {
background-color: #e0e0e0;
color: #333;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
}
.adjust-btn:hover {
background-color: #d5d5d5;
}
.main-controls {
display: flex;
justify-content: center;
gap: 1rem;
}
.control-btn {
min-width: 100px;
font-size: 1.1rem;
padding: 0.75rem 1.5rem;
}
.start-btn {
background-color: var(--primary-color);
color: white;
}
.start-btn:hover {
background-color: var(--primary-hover);
}
.reset-btn {
background-color: var(--secondary-color);
color: white;
}
.reset-btn:hover {
background-color: var(--secondary-hover);
}
@media (max-width: 480px) {
.timer-display {
font-size: 3rem;
}
.control-btn {
min-width: 80px;
padding: 0.6rem 1.2rem;
}
}