From 188c7d4bc6be17d88dd5011e7cc37ae3830cad90 Mon Sep 17 00:00:00 2001 From: David Fencl Date: Wed, 26 Nov 2025 21:13:10 +0100 Subject: [PATCH] chore(frontend): add time braker --- src/App.tsx | 13 ++- src/components/Home.tsx | 88 ++--------------- src/components/Navbar.tsx | 81 ++++++++++++++++ src/components/TimeBreaker.css | 162 +++++++++++++++++++++++++++++++ src/components/TimeBreaker.tsx | 163 +++++++++++++++++++++++++++++++ src/translations.ts | 38 ++++++++ work-break-timer/index.html | 44 +++++++++ work-break-timer/main.js | 149 +++++++++++++++++++++++++++++ work-break-timer/main.ts | 169 +++++++++++++++++++++++++++++++++ work-break-timer/styles.css | 144 ++++++++++++++++++++++++++++ 10 files changed, 969 insertions(+), 82 deletions(-) create mode 100644 src/components/Navbar.tsx create mode 100644 src/components/TimeBreaker.css create mode 100644 src/components/TimeBreaker.tsx create mode 100644 src/translations.ts create mode 100644 work-break-timer/index.html create mode 100644 work-break-timer/main.js create mode 100644 work-break-timer/main.ts create mode 100644 work-break-timer/styles.css diff --git a/src/App.tsx b/src/App.tsx index 3c75413..54d6612 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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('en') return ( <> {isLoading ? ( setIsLoading(false)} /> ) : ( - + + + + } /> + } /> + + )} ) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 6b63974..da02cdd 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -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('en'); +const Home: React.FC = ({ 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 (
- -
@@ -143,3 +68,4 @@ const Home: React.FC = () => { }; export default Home; + diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..ad46fa1 --- /dev/null +++ b/src/components/Navbar.tsx @@ -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 = ({ 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 ( + + ); +}; + +export default Navbar; diff --git a/src/components/TimeBreaker.css b/src/components/TimeBreaker.css new file mode 100644 index 0000000..97e3dc8 --- /dev/null +++ b/src/components/TimeBreaker.css @@ -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; + } +} \ No newline at end of file diff --git a/src/components/TimeBreaker.tsx b/src/components/TimeBreaker.tsx new file mode 100644 index 0000000..2ca4c3b --- /dev/null +++ b/src/components/TimeBreaker.tsx @@ -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 = ({ 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(null); + const intervalRef = useRef(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 ( +
+
+
+

{t.timeBreaker}

+ +
+ {formatTime(timeLeft)} +
+ +
+
+ { + const minutes = parseInt(e.target.value, 10); + setTimeLeft(minutes * 60); + if (minutes > 0) setIsTimeUp(false); + }} + className="time-slider" + /> +
+ 0m + 180m +
+
+ +
+ {!isRunning ? ( + + ) : ( + + )} + +
+
+
+
+
+ ); +}; + +export default TimeBreaker; diff --git a/src/translations.ts b/src/translations.ts new file mode 100644 index 0000000..a35761d --- /dev/null +++ b/src/translations.ts @@ -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', + } +}; diff --git a/work-break-timer/index.html b/work-break-timer/index.html new file mode 100644 index 0000000..cfd63b7 --- /dev/null +++ b/work-break-timer/index.html @@ -0,0 +1,44 @@ + + + + + + + Work Break Timer + + + + +
+

Work Break Timer

+ +
22:00
+ +
+
+ + + + +
+ +
+ + +
+
+
+ + + + + + + \ No newline at end of file diff --git a/work-break-timer/main.js b/work-break-timer/main.js new file mode 100644 index 0000000..019d533 --- /dev/null +++ b/work-break-timer/main.js @@ -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(); diff --git a/work-break-timer/main.ts b/work-break-timer/main.ts new file mode 100644 index 0000000..d41c518 --- /dev/null +++ b/work-break-timer/main.ts @@ -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(); diff --git a/work-break-timer/styles.css b/work-break-timer/styles.css new file mode 100644 index 0000000..e36ba84 --- /dev/null +++ b/work-break-timer/styles.css @@ -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; + } +}