ساخت ساعت پومودورو تعاملی و ذخیره آمار در یک فایل HTML

آموزش ساخت ساعت زنگ‌دار پومودوروی تعاملی با قابلیت ذخیره در LocalStorage و پخش صدای داخلی، در یک فایل واحد HTML، CSS و جاوا اسکریپت.

🎧 خلاصه صوتی مقاله (پادکست ۲ دقیقه‌ای):

فرصت مطالعه ندارید؟ چکیده این مطلب را همین‌جا گوش دهید:

مدیریت زمان و حفظ تمرکز در هنگام کار یا تحصیل یکی از بزرگ‌ترین چالش‌های برنامه‌نویسان، دانشجویان و افراد دورکار است. ابزارهای آنلاین زیادی وجود دارند که این کار را انجام می‌دهند، اما بیشتر آن‌ها پر از تبلیغات آزاردهنده هستند یا برای کارکردن نیاز به اتصال دائم به اینترنت دارند. تکنیک پومودورو (Pomodoro Technique) یکی از اثبات‌شده‌ترین روش‌ها برای افزایش بهره‌وری است که بر پایه‌ی تقسیم زمان به بازه‌های کاری متمرکز (معمولاً ۲۵ دقیقه‌ای) و استراحت‌های کوتاه بنا شده است. در این مقاله قصد داریم ساخت یک ساعت زنگ‌دار پومودوروی تعاملی، مینیمال و کاملاً رایگان را بررسی کنیم که تمام کدهای آن در یک فایل واحد جا گرفته و بدون نیاز به هیچ ابزار جانبی مستقیماً روی مرورگر شما اجرا می‌شود.

پیش از شروع ساخت این ابزار، یادآوری این نکته خالی از لطف نیست که تسلط بر ساختار و فرآیندهای فنی وب، شباهت زیادی به اصول پایه‌ای طراحی رابط کاربری دارد که در مقاله نقشه راه تسلط بر معماری اطلاعات سایت؛ هنر هدایت کاربر در ۳ ثانیه به تفصیل بررسی شده است. در پروژه‌ی پیش رو نیز تلاش کرده‌ایم تا با رعایت همین اصول، یک رابط کاربری ساده، خلوت و هدفمند طراحی کنیم تا کاربر در نخستین ثانیه‌ی ورود به صفحه، هدف ابزار را درک کند و درگیر پیچیدگی نشود.

نوع ابزارپروژه‌ی فرانت‌اند تک‌فایلی
فناوری‌های به‌کاررفتهHTML5, CSS3, JavaScript (Vanilla)
قابلیت‌های کلیدیتایمر دوطرفه، ذخیره آمار در LocalStorage، تولید صدای هشدار با Web Audio API
هزینه۱۰۰٪ رایگان و متن‌باز
وضعیت دسترسی ایرانبدون محدودیت (اجرای محلی روی مرورگر)

چرا این ابزار پومودورو با بقیه فرق دارد؟

بسیاری از اپلیکیشن‌های تحت وب برای پخش صدای هشدار پایان زمان، وابسته به فایل‌های صوتی خارجی (مثل فایل‌های MP3) هستند که اگر سرور میزبان دچار مشکل شود یا اینترنت قطع گردد، سیستم صوتی از کار می‌افتد. در پروژه‌ای که امروز پیاده‌سازی می‌کنیم، از قابلیت پیشرفته‌ی مرورگر یعنی Web Audio API استفاده شده است. این یعنی مرورگر شما به‌صورت کاملاً داخلی و با استفاده از کدهای جاوا اسکریپت، موج صوتی ملایمی را در لحظه‌ی اتمام زمان تولید و پخش می‌کند و هیچ نیازی به دانلود یا بارگذاری فایل صوتی جانبی نیست.

علاوه بر این، این ابزار مجهز به سیستم ذخیره‌سازی محلی یا همان LocalStorage است. تمام پومودوروهایی که در طول روز با موفقیت تکمیل می‌کنید، در حافظه‌ی مرورگر شما ثبت می‌شوند و حتی با بستن تب یا رفرش‌کردن صفحه نیز از بین نمی‌روند. این ویژگی به شما اجازه می‌دهد بدون نیاز به ثبت‌نام در سایت‌های مختلف، آمار روزانه‌ی کارکرد خود را ردیابی کنید. طراحی ظاهر این ابزار با حالت تاریک (Dark Mode) پیش‌فرض انجام شده است تا در استفاده‌های طولانی‌مدت شبانه، چشم کاربران خسته نشود.

کد کامل برنامه‌ی پومودورو در یک فایل

برای اجرای این ابزار، کافی است یک فایل متنی جدید در کامپیوتر خود ایجاد کنید، نام آن را مثلاً pomodoro.html بگذارید و کدهای زیر را به‌صورت کامل درون آن کپی کنید. سپس فایل را ذخیره کرده و با هر مرورگری (کروم، فایرفاکس، اج و غیره) آن را باز کنید تا جادوی کار را ببینید.

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ساعت پومودورو مینیمال</title>
    <style>
        :root {
            --bg-color: #0f172a;
            --card-bg: #1e293b;
            --text-color: #f8fafc;
            --text-muted: #94a3b8;
            --primary: #6366f1;
            --primary-hover: #4f46e5;
            --accent: #10b981;
            --danger: #ef4444;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        }

        body {
            background-color: var(--bg-color);
            color: var(--text-color);
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }

        .container {
            background-color: var(--card-bg);
            padding: 2.5rem;
            border-radius: 1.5rem;
            box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
            width: 100%;
            max-width: 420px;
            text-align: center;
        }

        h1 {
            font-size: 1.5rem;
            margin-bottom: 1.5rem;
            color: var(--text-color);
        }

        .modes {
            display: flex;
            justify-content: center;
            gap: 0.5rem;
            margin-bottom: 2rem;
            background: rgba(255, 255, 255, 0.03);
            padding: 0.4rem;
            border-radius: 1rem;
        }

        .mode-btn {
            background: transparent;
            border: none;
            color: var(--text-muted);
            padding: 0.6rem 1rem;
            border-radius: 0.7rem;
            cursor: pointer;
            font-size: 0.9rem;
            font-weight: 600;
            transition: all 0.2s ease;
            flex: 1;
        }

        .mode-btn.active {
            background-color: var(--primary);
            color: white;
        }

        .timer-display {
            font-size: 4.5rem;
            font-weight: 800;
            letter-spacing: -2px;
            margin-bottom: 2rem;
            font-variant-numeric: tabular-nums;
        }

        .controls {
            display: flex;
            gap: 1rem;
            margin-bottom: 2rem;
        }

        .btn {
            flex: 1;
            padding: 0.9rem;
            border: none;
            border-radius: 0.8rem;
            font-size: 1rem;
            font-weight: 700;
            cursor: pointer;
            transition: opacity 0.2s;
        }

        .btn:active {
            transform: scale(0.98);
        }

        .btn-main {
            background-color: var(--accent);
            color: white;
        }

        .btn-main.running {
            background-color: var(--danger);
        }

        .btn-reset {
            background-color: rgba(255, 255, 255, 0.05);
            color: var(--text-muted);
        }

        .stats {
            border-top: 1px solid rgba(255, 255, 255, 0.05);
            padding-top: 1.5rem;
            font-size: 0.95rem;
            color: var(--text-muted);
        }

        .stats span {
            color: var(--accent);
            font-weight: bold;
        }
    </style>
</head>
<body>

    <div class="container">
        <h1>ساعت پومودورو</h1>
        
        <div class="modes">
            <button class="mode-btn active" onclick="setMode('work', 25, this)">کار (۲۵ مین)</button>
            <button class="mode-btn" onclick="setMode('shortBreak', 5, this)">استراحت کوتاه</button>
            <button class="mode-btn" onclick="setMode('longBreak', 15, this)">استراحت بلند</button>
        </div>

        <div class="timer-display" id="timer">25:00</div>

        <div class="controls">
            <button class="btn btn-main" id="startBtn" onclick="toggleTimer()">شروع</button>
            <button class="btn btn-reset" onclick="resetTimer()">بازنشانی</button>
        </div>

        <div class="stats">
            تعداد پومودوروهای تکمیل‌شده امروز: <span id="countDisplay">0</span>
        </div>
    </div>

    <script>
        let timeLeft = 25 * 60;
        let timerId = null;
        let isRunning = false;
        let currentMode = 'work';

        const timerDisplay = document.getElementById('timer');
        const startBtn = document.getElementById('startBtn');
        const countDisplay = document.getElementById('countDisplay');

        // خواندن تعداد پومودوروهای ذخیره شده از LocalStorage
        let completedCount = localStorage.getItem('pomodoroCount') ? parseInt(localStorage.getItem('pomodoroCount')) : 0;
        countDisplay.textContent = completedCount;

        function updateDisplay() {
            const minutes = Math.floor(timeLeft / 60);
            const seconds = timeLeft % 60;
            timerDisplay.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
        }

        function setMode(mode, minutes, element) {
            stopTimer();
            currentMode = mode;
            timeLeft = minutes * 60;
            updateDisplay();

            document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
            element.classList.add('active');
        }

        function toggleTimer() {
            if (isRunning) {
                stopTimer();
            } else {
                startTimer();
            }
        }

        function startTimer() {
            if (isRunning) return;
            isRunning = true;
            startBtn.textContent = 'توقف';
            startBtn.classList.add('running');

            timerId = setInterval(() => {
                if (timeLeft > 0) {
                    timeLeft--;
                    updateDisplay();
                } else {
                    stopTimer();
                    playAlarm();
                    if (currentMode === 'work') {
                        completedCount++;
                        localStorage.setItem('pomodoroCount', completedCount);
                        countDisplay.textContent = completedCount;
                    }
                    alert('زمان به پایان رسید!');
                }
            }, 1000);
        }

        function stopTimer() {
            isRunning = false;
            clearInterval(timerId);
            startBtn.textContent = 'شروع';
            startBtn.classList.remove('running');
        }

        function resetTimer() {
            stopTimer();
            if (currentMode === 'work') timeLeft = 25 * 60;
            else if (currentMode === 'shortBreak') timeLeft = 5 * 60;
            else if (currentMode === 'longBreak') timeLeft = 15 * 60;
            updateDisplay();
        }

        function playAlarm() {
            try {
                const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                const oscillator = audioCtx.createOscillator();
                const gainNode = audioCtx.createGain();

                oscillator.type = 'sine';
                oscillator.frequency.setValueAtTime(587.33, audioCtx.currentTime); // نت D5
                gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);

                oscillator.connect(gainNode);
                gainNode.connect(audioCtx.destination);

                oscillator.start();
                oscillator.stop(audioCtx.currentTime + 0.8);
            } catch (e) {
                console.log('پخش صوتی پشتیبانی نمی‌شود یا مسدود شده است.');
            }
        }

        updateDisplay();
    </script>
</body>
</html>

بررسی بخش‌به‌بخش ساختار کد

این کد به‌گونه‌ای نوشته شده است که کمترین حجم و بیشترین خوانایی را داشته باشد. در ادامه اجزای کلیدی آن را بررسی می‌کنیم:

  • بخش استایل (CSS Variables): در بخش ابتدایی تگ <style> متغیرهای رنگی گلچین شده‌اند تا اگر خواستید تم رنگی تیره را تغییر دهید، تنها با ویرایش متغیرهای متناظر در بخش ریشه (:root) کل ظاهر برنامه هماهنگ شود. فونت سیستم (System UI) به کار رفته باعث می‌شود بدون نیاز به دانلود فونت‌های حجیم اینترنتی، متن‌ها با زیباترین فونت پیش‌فرض دستگاه شما نمایش داده شوند.
  • منوی حالت‌ها (Modes): سه دکمه برای حالت‌های کار (۲۵ دقیقه)، استراحت کوتاه (۵ دقیقه) و استراحت بلند (۱۵ دقیقه) در نظر گرفته شده است. با کلیک روی هر دکمه، تابع setMode فراخوانی می‌شود که هم‌زمان تایمر را ریست کرده، زمان جدید را تنظیم و کلاس ظاهری دکمه‌ی فعال را جابه‌جا می‌کند.
  • موتور زمان‌سنج (JavaScript Interval): تابع setInterval وظیفه‌ی کاهش ثانیه‌به‌ثانیه‌ی متغیر timeLeft را بر عهده دارد. تابع کمکی padStart تضمین می‌کند که اعداد تک‌رقمی همیشه با یک صفر در ابتدا نمایش داده شوند (مثلاً 05:09 به جای 5:9).
  • ذخیره‌سازی مداوم (LocalStorage): هر بار که تایمر حالت کار به اتمام می‌رسد، مقدار متغیر completedCount یک واحد افزایش یافته و بلافاصله با دستور localStorage.setItem در حافظه‌ی مرورگر ثبت می‌شود. هنگام لود شدن صفحه نیز مقدار قبلی خوانده و در عنصر مربوطه جایگذاری می‌گردد.

چگونه از این ابزار بیشترین بهره را ببریم؟

برای استفاده‌ی بهینه، کافی است فایل خروجی را در یک پوشه‌ی کاری ثابت روی سیستم خود قرار دهید و میانبر آن را دم دست بگذارید. از آنجا که هیچ وابستگی به سرورهای خارجی ندارد، حتی در حالت آفلاینِ کامل نیز بدون نقص کار می‌کند.

رعایت انضباط در زمان‌های استراحت و کار، بازدهی مغز شما را به شدت بالا می‌برد. همان‌طور که مدیریت زمان نیازمند نظم است، سازماندهی داده‌ها و اطلاعات دیجیتال شخصی نیز اهمیت بالایی دارد؛ مقالاتی نظیر مدیریت بودجه ماهانه شخصی با نمودارهای گیمیفاید در اکسل نمونه‌های دیگری از ابزارهای سبک و کاربردی شخصی هستند که بدون پیچیدگی به نظم‌دهی امور روزمره کمک می‌کنند.

اشتباهات رایج در پیاده‌سازی تایمرهای جاوا اسکریپتی

اگر خودتان قصد دارید این کد را توسعه دهید یا بخش‌هایی به آن اضافه کنید، مراقب این تله‌های رایج باشید:

  • مدیریت نکردن چندبار کلیک روی دکمه‌ی شروع: اگر کاربر چند بار پشت سر هم روی دکمه‌ی «شروع» کلیک کند، چندین اینتروال (Interval) هم‌زمان ساخته می‌شوند و تایمر با سرعت غیرعادی کم می‌شود. در کد بالا با بررسی شرط if (isRunning) return; این مشکل به‌کل حل شده است.
  • تداخل حافظه لوکال‌استوریج: مقدار ذخیره‌شده در LocalStorage همیشه به شکل رشته (String) است. اگر پیش از عمل جمع، از تابع parseInt استفاده نکنید، ممکن است به جای جمع ریاضی، اعداد کنار هم بچسبند (مثلاً نتیجه‌ی 1 + 1 به جای 2 شود 11). در کدهای بالا این نکته رعایت شده است.

پرسش‌های متداول

آیا این فایل نیاز به نصب نرم‌افزار خاصی دارد؟

خیر. تنها چیزی که نیاز دارید یک مرورگر وب مدرن (مثل کروم، فایرفاکس، سافاری یا اج) است. هیچ پکیج، کامپایلر یا برنامه‌ی جانبی دیگری نیاز نیست.

چگونه می‌توانم زمان‌های پیش‌فرض کار یا استراحت را تغییر دهم؟

کافی است فایل HTML را با یک ویرایشگر متن (مثل Notepad یا VS Code) باز کنید و در خط مربوط به دکمه‌ها، عددِ دقایق را (مثلاً عبارت 25 یا 5) به مقدار دلخواه خود تغییر دهید.

آیا آمار پومودوروهای من با بستن مرورگر پاک می‌شود؟

خیر. به لطف استفاده از حافظه‌ی محلی مرورگر (LocalStorage)، آمار تعداد پومودورهای تکمیل‌شده حفظ خواهد شد، مگر اینکه کش یا داده‌های مرورگر خود را به‌صورت دستی پاک کنید.

چرا صدای هشدار در برخی مرورگرها پخش نمی‌شود؟

برخی مرورگرها برای جلوگیری از ایجاد مزاحمت صوتی توسط صفحات وب، پخش خودکار صدا را تا پیش از یک تعامل مستقیم کاربر (مثل کلیک روی صفحه) مسدود می‌کنند. از آنجا که دکمه‌ی هشدار پس از کلیک روی شروع فعال می‌شود، این محدودیت عموماً ایجاد نمی‌شود.

جمع‌بندی

ساخت ابزارهای سبک، بدون تبلیغات و متکی بر مرورگر یکی از بهترین راه‌ها برای کنترل ابزارها و فرار از حواس‌پرتی‌های دیجیتال است. در این مقاله یاد گرفتیم چگونه با ترکیب ساده‌ی HTML ،CSS مدرن و جاوا اسکریپت خالص، یک ساعت پومودوروی استاندارد با قابلیت تولید صدای هشدار داخلی و ثبت آمار روزانه در یک فایل واحد بسازیم. با کپی کردن این کد، هم‌اکنون یک ابزار بهره‌وری اختصاصی و سریع در اختیار دارید که می‌توانید هر زمان که خواستید آن را مطابق با سلیقه‌ی خود شخصی‌سازی کنید.

پاسخی بگذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *