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

@@ -1,17 +1,28 @@
import { useState } from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import LoadingScreen from './components/LoadingScreen'
import Home from './components/Home'
import Navbar from './components/Navbar'
import TimeBreaker from './components/TimeBreaker'
import { type Language } from './translations'
import './App.css'
function App() {
const [isLoading, setIsLoading] = useState(true)
const [language, setLanguage] = useState<Language>('en')
return (
<>
{isLoading ? (
<LoadingScreen onLoaded={() => setIsLoading(false)} />
) : (
<Home />
<Router>
<Navbar language={language} setLanguage={setLanguage} />
<Routes>
<Route path="/" element={<Home language={language} />} />
<Route path="/time-breaker" element={<TimeBreaker language={language} />} />
</Routes>
</Router>
)}
</>
)

View File

@@ -1,92 +1,17 @@
import React, { useState } from 'react';
import logo from '../assets/logo.jpg';
import React from 'react';
import { FaFacebookF, FaInstagram, FaLinkedinIn } from 'react-icons/fa';
import { SiTypescript, SiReact, SiJavascript } from 'react-icons/si';
import { translations, type Language } from '../translations';
type Language = 'en' | 'cs';
interface HomeProps {
language: Language;
}
const translations = {
en: {
home: 'Home',
about: 'About',
contact: 'Contact',
welcome: 'WELCOME TO MY WORLD',
hello: "Hello, I'm",
job: 'a Developer.',
desc: "I use animation as a third dimension by which to simplify experiences and guiding through each and every interaction. I'm not adding motion just to spruce things up, but doing it in ways that matter.",
findMe: 'FIND WITH ME',
bestSkill: 'BEST SKILL ON',
aboutMe: 'About Me',
aboutDesc: 'I build accessible, pixel-perfect, and performant web experiences. Passionate about technology and design.',
getInTouch: 'Get In Touch',
contactDesc: 'Interested in working together?',
emailMe: 'Email me',
},
cs: {
home: 'Domů',
about: 'O mně',
contact: 'Kontakt',
welcome: 'VÍTEJTE V MÉM SVĚTĚ',
hello: "Ahoj, jsem",
job: 'Vývojář.',
desc: "Používám animaci jako třetí rozměr, kterým zjednodušuji zážitky a provázím každou interakcí. Nepřidávám pohyb jen pro efekt, ale dělám to způsoby, které mají smysl.",
findMe: 'NAJDETE MĚ NA',
bestSkill: 'DOVEDNOSTI',
aboutMe: 'O mně',
aboutDesc: 'Tvořím přístupné, pixel-perfect a výkonné webové zážitky. Vášnivý pro technologie a design.',
getInTouch: 'Napište mi',
contactDesc: 'Máte zájem o spolupráci?',
emailMe: 'Napište mi',
}
};
const Home: React.FC = () => {
const [language, setLanguage] = useState<Language>('en');
const Home: React.FC<HomeProps> = ({ language }) => {
const t = translations[language];
const handleNavClick = (id: string, e: React.MouseEvent) => {
e.preventDefault();
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<div className="home-container fade-in">
<nav className="navbar">
<div className="nav-content">
<div
className="logo-container"
onClick={(e) => handleNavClick('home', e)}
style={{ cursor: 'pointer' }}
>
<img src={logo} alt="David Fencl Logo" className="nav-logo" />
</div>
<div className="nav-right">
<div className="language-switcher">
<button
className={`lang-btn ${language === 'en' ? 'active' : ''}`}
onClick={() => setLanguage('en')}
>
EN
</button>
<span className="lang-separator">/</span>
<button
className={`lang-btn ${language === 'cs' ? 'active' : ''}`}
onClick={() => setLanguage('cs')}
>
CZ
</button>
</div>
<div className="links">
<a href="#about" onClick={(e) => handleNavClick('about', e)}>{t.about}</a>
<a href="#contact" onClick={(e) => handleNavClick('contact', e)}>{t.contact}</a>
</div>
</div>
</div>
</nav>
<section id="home" className="hero fade-in">
<div className="hero-content">
<div className="hero-text">
@@ -143,3 +68,4 @@ const Home: React.FC = () => {
};
export default Home;

81
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,81 @@
import React from 'react';
import logo from '../assets/logo.jpg';
import { translations, type Language } from '../translations';
import { Link, useLocation, useNavigate } from 'react-router-dom';
interface NavbarProps {
language: Language;
setLanguage: (lang: Language) => void;
}
const Navbar: React.FC<NavbarProps> = ({ language, setLanguage }) => {
const t = translations[language];
const location = useLocation();
const navigate = useNavigate();
const handleNavClick = (id: string, e: React.MouseEvent) => {
e.preventDefault();
if (location.pathname !== '/') {
navigate('/');
// Wait for navigation then scroll
setTimeout(() => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
} else {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
};
const handleHomeClick = (e: React.MouseEvent) => {
e.preventDefault();
if (location.pathname !== '/') {
navigate('/');
} else {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
};
return (
<nav className="navbar">
<div className="nav-content">
<div
className="logo-container"
onClick={handleHomeClick}
style={{ cursor: 'pointer' }}
>
<img src={logo} alt="David Fencl Logo" className="nav-logo" />
</div>
<div className="nav-right">
<div className="language-switcher">
<button
className={`lang-btn ${language === 'en' ? 'active' : ''}`}
onClick={() => setLanguage('en')}
>
EN
</button>
<span className="lang-separator">/</span>
<button
className={`lang-btn ${language === 'cs' ? 'active' : ''}`}
onClick={() => setLanguage('cs')}
>
CZ
</button>
</div>
<div className="links">
<Link to="/time-breaker">{t.timeBreaker}</Link>
<a href="#about" onClick={(e) => handleNavClick('about', e)}>{t.about}</a>
<a href="#contact" onClick={(e) => handleNavClick('contact', e)}>{t.contact}</a>
</div>
</div>
</div>
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,162 @@
.timer-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 500px;
margin: 0 auto;
padding: 2rem;
background-color: rgba(30, 32, 36, 0.5);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
}
.timer-display {
font-size: 5rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
margin: 2rem 0;
padding: 1rem 2rem;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.2);
color: var(--text-color);
transition: background-color 0.3s ease, color 0.3s ease;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.timer-display.time-up {
background-color: rgba(244, 67, 54, 0.2);
color: #f44336;
animation: flash 1s infinite;
border: 1px solid rgba(244, 67, 54, 0.5);
}
@keyframes flash {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.8;
}
}
.controls-section {
display: flex;
flex-direction: column;
gap: 2rem;
width: 100%;
}
.slider-container {
width: 100%;
padding: 0 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.time-slider {
-webkit-appearance: none;
width: 100%;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
outline: none;
cursor: pointer;
}
.time-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--accent-color);
cursor: pointer;
transition: transform 0.2s;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.time-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
background: #747bff;
}
.time-slider::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--accent-color);
cursor: pointer;
border: none;
transition: transform 0.2s;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.time-slider::-moz-range-thumb:hover {
transform: scale(1.1);
background: #747bff;
}
.slider-labels {
display: flex;
justify-content: space-between;
color: var(--secondary-color);
font-size: 0.8rem;
font-weight: 500;
}
.main-controls {
display: flex;
justify-content: center;
gap: 1.5rem;
}
.control-btn {
min-width: 120px;
font-size: 1.1rem;
padding: 0.8rem 2rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.start-btn {
background-color: var(--accent-color);
color: white;
box-shadow: 0 4px 15px rgba(100, 108, 255, 0.3);
}
.start-btn:hover {
background-color: #747bff;
box-shadow: 0 6px 20px rgba(100, 108, 255, 0.4);
transform: translateY(-2px);
}
.reset-btn {
background-color: transparent;
border: 1px solid var(--secondary-color);
color: var(--secondary-color);
}
.reset-btn:hover {
border-color: var(--text-color);
color: var(--text-color);
background-color: rgba(255, 255, 255, 0.05);
}
@media (max-width: 480px) {
.timer-display {
font-size: 3.5rem;
}
.control-btn {
min-width: 100px;
padding: 0.7rem 1.5rem;
font-size: 1rem;
}
}

View File

@@ -0,0 +1,163 @@
import React, { useState, useEffect, useRef } from 'react';
import { translations, type Language } from '../translations';
import './TimeBreaker.css';
interface TimeBreakerProps {
language: Language;
}
const DEFAULT_TIME_MINUTES = 22;
const MAX_TIME_MINUTES = 180;
const SECONDS_PER_MINUTE = 60;
const TimeBreaker: React.FC<TimeBreakerProps> = ({ language }) => {
const t = translations[language];
const [timeLeft, setTimeLeft] = useState(DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE);
const [isRunning, setIsRunning] = useState(false);
const [isTimeUp, setIsTimeUp] = useState(false);
const audioContextRef = useRef<AudioContext | null>(null);
const intervalRef = useRef<number | null>(null);
// Format time as MM:SS
const 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')}`;
};
// Update document title
useEffect(() => {
document.title = `${formatTime(timeLeft)} - ${t.timeBreaker}`;
return () => {
document.title = 'David Fencl - IT Consulting';
};
}, [timeLeft, t.timeBreaker]);
// Timer logic
useEffect(() => {
if (isRunning && timeLeft > 0) {
intervalRef.current = window.setInterval(() => {
setTimeLeft((prev) => {
if (prev <= 1) {
handleTimeUp();
return 0;
}
return prev - 1;
});
}, 1000);
} else if (timeLeft === 0 && isRunning) {
handleTimeUp();
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isRunning]);
const handleTimeUp = () => {
setIsRunning(false);
setIsTimeUp(true);
playAlarm();
// Play alarm 3 times
setTimeout(playAlarm, 1000);
setTimeout(playAlarm, 2000);
};
const playAlarm = () => {
if (!audioContextRef.current) {
audioContextRef.current = new (window.AudioContext || (window as any).webkitAudioContext)();
}
const ctx = audioContextRef.current;
if (!ctx) return;
// Create oscillator
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(880, ctx.currentTime); // A5
oscillator.frequency.exponentialRampToValueAtTime(440, ctx.currentTime + 0.5); // Drop to A4
gainNode.gain.setValueAtTime(0.5, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.start();
oscillator.stop(ctx.currentTime + 0.5);
};
const handleStart = () => {
if (timeLeft === 0) return;
setIsRunning(true);
setIsTimeUp(false);
};
const handlePause = () => {
setIsRunning(false);
};
const handleReset = () => {
setIsRunning(false);
setIsTimeUp(false);
setTimeLeft(DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE);
};
return (
<div className="home-container fade-in">
<section className="hero">
<div className="timer-container">
<h1 style={{ marginBottom: '1rem' }}>{t.timeBreaker}</h1>
<div className={`timer-display ${isTimeUp ? 'time-up' : ''}`}>
{formatTime(timeLeft)}
</div>
<div className="controls-section">
<div className="slider-container">
<input
type="range"
min="0"
max="180"
value={Math.ceil(timeLeft / 60)}
onChange={(e) => {
const minutes = parseInt(e.target.value, 10);
setTimeLeft(minutes * 60);
if (minutes > 0) setIsTimeUp(false);
}}
className="time-slider"
/>
<div className="slider-labels">
<span>0m</span>
<span>180m</span>
</div>
</div>
<div className="main-controls">
{!isRunning ? (
<button className="btn control-btn start-btn" onClick={handleStart}>
{timeLeft > 0 && timeLeft < DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE ? 'Resume' : 'Start'}
</button>
) : (
<button className="btn control-btn start-btn" onClick={handlePause}>
Pause
</button>
)}
<button className="btn control-btn reset-btn" onClick={handleReset}>
Reset
</button>
</div>
</div>
</div>
</section>
</div>
);
};
export default TimeBreaker;

38
src/translations.ts Normal file
View File

@@ -0,0 +1,38 @@
export type Language = 'en' | 'cs';
export const translations = {
en: {
home: 'Home',
about: 'About',
contact: 'Contact',
timeBreaker: 'Time-Breaker',
welcome: 'WELCOME TO MY WORLD',
hello: "Hello, I'm",
job: 'a Developer.',
desc: "I use animation as a third dimension by which to simplify experiences and guiding through each and every interaction. I'm not adding motion just to spruce things up, but doing it in ways that matter.",
findMe: 'FIND WITH ME',
bestSkill: 'BEST SKILL ON',
aboutMe: 'About Me',
aboutDesc: 'I build accessible, pixel-perfect, and performant web experiences. Passionate about technology and design.',
getInTouch: 'Get In Touch',
contactDesc: 'Interested in working together?',
emailMe: 'Email me',
},
cs: {
home: 'Domů',
about: 'O mně',
contact: 'Kontakt',
timeBreaker: 'Time-Breaker',
welcome: 'VÍTEJTE V MÉM SVĚTĚ',
hello: "Ahoj, jsem",
job: 'Vývojář.',
desc: "Používám animaci jako třetí rozměr, kterým zjednodušuji zážitky a provázím každou interakcí. Nepřidávám pohyb jen pro efekt, ale dělám to způsoby, které mají smysl.",
findMe: 'NAJDETE MĚ NA',
bestSkill: 'DOVEDNOSTI',
aboutMe: 'O mně',
aboutDesc: 'Tvořím přístupné, pixel-perfect a výkonné webové zážitky. Vášnivý pro technologie a design.',
getInTouch: 'Napište mi',
contactDesc: 'Máte zájem o spolupráci?',
emailMe: 'Napište mi',
}
};

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;
}
}