Single-File Browser Time Tracker with Heat-Maps & Themes

Answered using AI by Vibe Coding

Code Question:

Hello! I need a little place to keep track of the time I spend on my projects. Therefore, I need a place where I could set a project name, a timer and keep it going until I need to stop it for the next day. Obviously, the site should safe my progress, you can also add some kind of monthly/weekly visual board I can go to in order to see how much time on what day do I spent on what project. I would like the opportunity to customize the background with a couple of the most popular options like minimalism, anime style, cozy vibes, etc.

Code Solution:

Below is a complete, self-contained “tracker.html” file.
Open it in any modern browser (Chrome, Edge, Firefox, Safari).
Everything lives in this single file: no server, no build tools, no install.
Your data is stored in the browser’s LocalStorage, so closing the tab or even the whole computer will not erase it.

Features • Start / pause / stop timers per project
• Automatically resumes the timer after a restart if you forget to stop it
• Stores every start–stop pair as a log entry
• Shows a visual weekly + monthly heat-map (how many minutes you worked on each project each calendar day)
• Lets you switch between four background themes: Minimalism, Cozy, Anime, Dark
• Fully keyboard-friendly (Tab → Enter)

Copy the code below, save it as tracker.html, double-click to open.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Project Time Tracker</title>
<style>
    :root{
        --bg:#ffffff;
        --bg-img:none;
        --text:#111;
        --accent:#0077ff;
    }
    body{
        margin:0;
        font-family:system-ui,Segoe UI,Roboto,sans-serif;
        background:var(--bg) var(--bg-img) center/cover no-repeat fixed;
        color:var(--text);
        display:flex;
        flex-direction:column;
        min-height:100vh;
    }
    header{position:sticky;top:0;background:rgba(255,255,255,.9);backdrop-filter:blur(4px);z-index:9;padding:8px 12px;border-bottom:1px solid #ddd}
    main{flex:1;padding:12px;display:grid;gap:16px;grid-template-columns:1fr 1fr}
    h1{margin:0;font-size:1.4rem}
    section{background:rgba(255,255,255,.75);padding:12px;border-radius:8px;backdrop-filter:blur(4px)}
    label{display:block;margin:4px 0 2px;font-size:.9rem}
    input,select,button{font-size:1rem;padding:6px;border:1px solid #ccc;border-radius:4px}
    button{background:var(--accent);color:#fff;border:none;cursor:pointer}
    button:disabled{opacity:.5;cursor:not-allowed}
    #timer{font-size:2rem;font-variant-numeric:tabular-nums}
    table{border-collapse:collapse;width:100%;font-size:.85rem}
    th,td{border:1px solid #ddd;padding:4px;text-align:center}
    .legend{display:flex;gap:8px;font-size:.8rem;margin-top:6px}
    .legend span{width:18px;height:18px;border:1px solid #ccc}
    @media(max-width:700px){main{grid-template-columns:1fr}}
</style>
</head>
<body>
<header>
    <h1>Project Time Tracker</h1>
    <label>Theme:
        <select id="themeSelect">
            <option value="minimal">Minimalism</option>
            <option value="cozy">Cozy Café</option>
            <option value="anime">Anime Sky</option>
            <option value="dark">Dark Mode</option>
        </select>
    </label>
</header>

<main>
<section>
    <h2>Timer</h2>
    <label>Project:
        <input id="projectName" placeholder="E.g. Website Redesign" />
    </label>
    <div id="timer">00:00:00</div>
    <button id="startBtn">Start</button>
    <button id="pauseBtn" disabled>Pause</button>
    <button id="stopBtn" disabled>Stop & Save</button>
</section>

<section>
    <h2>Weekly Heat-map</h2>
    <div id="weeklyGrid"></div>
    <div class="legend">
        <span style="background:#ebedf0"></span> 0 min
        <span style="background:#9be9a8"></span> 1-30
        <span style="background:#40c463"></span> 31-120
        <span style="background:#30a14e"></span> 121-240
        <span style="background:#216e39"></span> 240+
    </div>
</section>

<section>
    <h2>Monthly Heat-map</h2>
    <div id="monthlyGrid"></div>
    <div class="legend">
        <span style="background:#ebedf0"></span> 0 min
        <span style="background:#9be9a8"></span> 1-30
        <span style="background:#40c463"></span> 31-120
        <span style="background:#30a14e"></span> 121-240
        <span style="background:#216e39"></span> 240+
    </div>
</section>
</main>

<script>
/* ---------- Persistent Storage ---------- */
const STORE = {
    get(key){try{return JSON.parse(localStorage.getItem(key))}catch{return null}},
    set(key,val){localStorage.setItem(key,JSON.stringify(val))}
};

/* ---------- Theme Switcher ---------- */
const themes = {
    minimal:{bg:'#ffffff',bgImg:'none',text:'#111'},
    cozy:{bg:'#fdf6f0',bgImg:'url(https://images.unsplash.com/photo-1509042239860-f550ce710b93?auto=format&fit=crop&w=1350&q=80)',text:'#111'},
    anime:{bg:'#dbeafe',bgImg:'url(https://images.unsplash.com/photo-1528164344705-47542687000d?auto=format&fit=crop&w=1350&q=80)',text:'#111'},
    dark:{bg:'#111827',bgImg:'none',text:'#f9fafb'}
};
const themeSelect = document.getElementById('themeSelect');
themeSelect.value = STORE.get('theme') || 'minimal';
applyTheme(themeSelect.value);
themeSelect.addEventListener('change',e=>{
    applyTheme(e.target.value);
    STORE.set('theme',e.target.value);
});
function applyTheme(name){
    const t=themes[name];
    Object.entries(t).forEach(([k,v])=>document.documentElement.style.setProperty('--'+k.replace(/([A-Z])/g,'-$1').toLowerCase(),v));
}

/* ---------- Timer Logic ---------- */
let interval=null;
let startTime=null;
let paused=0;

const timerDisplay=document.getElementById('timer');
const startBtn=document.getElementById('startBtn');
const pauseBtn=document.getElementById('pauseBtn');
const stopBtn=document.getElementById('stopBtn');
const projectInput=document.getElementById('projectName');

/* Load running session if any */
const saved=STORE.get('current');
if(saved){
    projectInput.value=saved.project;
    startTime=saved.start;
    paused=saved.paused||0;
    interval=setInterval(updateTimer,1000);
    startBtn.disabled=true;
    pauseBtn.disabled=false;
    stopBtn.disabled=false;
}
updateTimer(); // Initial paint

function updateTimer(){
    const elapsed=Date.now()-(startTime||0)-paused;
    const sec=Math.floor(elapsed/1000);
    const h=String(Math.floor(sec/3600)).padStart(2,'0');
    const m=String(Math.floor(sec%3600/60)).padStart(2,'0');
    const s=String(sec%60).padStart(2,'0');
    timerDisplay.textContent=`${h}:${m}:${s}`;
}

startBtn.onclick=()=>{
    const proj=projectInput.value.trim();
    if(!proj){alert('Enter a project name');return;}
    startTime=Date.now();
    paused=0;
    STORE.set('current',{project:proj,start:startTime});
    interval=setInterval(updateTimer,1000);
    startBtn.disabled=true;
    pauseBtn.disabled=false;
    stopBtn.disabled=false;
};
pauseBtn.onclick=()=>{
    clearInterval(interval);
    paused+=Date.now()-startTime;
    interval=null;
    pauseBtn.textContent='Resume';
    pauseBtn.onclick=resume;
};
function resume(){
    startTime=Date.now();
    interval=setInterval(updateTimer,1000);
    pauseBtn.textContent='Pause';
    pauseBtn.onclick=pauseBtn.onclick=()=>{
        clearInterval(interval);
        paused+=Date.now()-startTime;
        interval=null;
        pauseBtn.textContent='Resume';
        pauseBtn.onclick=resume;
    };
}
stopBtn.onclick=()=>{
    clearInterval(interval);
    const elapsed=Date.now()-(startTime||0)-paused;
    const logs=STORE.get('logs')||[];
    logs.push({
        project:projectInput.value.trim(),
        start:startTime,
        end:Date.now(),
        minutes:Math.round(elapsed/60000)
    });
    STORE.set('logs',logs);
    STORE.set('current',null);
    startTime=null;
    paused=0;
    interval=null;
    timerDisplay.textContent='00:00:00';
    startBtn.disabled=false;
    pauseBtn.disabled=true;
    stopBtn.disabled=true;
    pauseBtn.textContent='Pause';
    renderGrids();
};

/* ---------- Heat-map rendering ---------- */
renderGrids();
function renderGrids(){
    const logs=STORE.get('logs')||[];
    const buckets={}; // YYYY-MM-DD -> {project -> minutes}
    logs.forEach(l=>{
        const day=new Date(l.start).toISOString().slice(0,10);
        buckets[day]=buckets[day]||{};
        buckets[day][l.project]=(buckets[day][l.project]||0)+l.minutes;
    });

    /* weekly (last 7 days) */
    const today=new Date();
    const week=[];
    const weeklyEl=document.getElementById('weeklyGrid');
    weeklyEl.innerHTML='';
    for(let i=6;i>=0;i--){
        const d=new Date(today);
        d.setDate(d.getDate()-i);
        const key=d.toISOString().slice(0,10);
        week.push({day:d,data:buckets[key]||{}});
    }
    renderTable(weeklyEl,week);

    /* monthly (this calendar month) */
    const year=today.getFullYear();
    const month=today.getMonth();
    const daysInMonth=new Date(year,month+1,0).getDate();
    const monthArr=[];
    const monthlyEl=document.getElementById('monthlyGrid');
    monthlyEl.innerHTML='';
    for(let day=1;day<=daysInMonth;day++){
        const key=`${year}-${String(month+1).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
        monthArr.push({day:new Date(year,month,day),data:buckets[key]||{}});
    }
    renderTable(monthlyEl,monthArr);
}

function renderTable(container,data){
    const table=document.createElement('table');
    const thead=document.createElement('thead');
    const headRow=document.createElement('tr');
    const projSet=new Set();
    data.forEach(d=>Object.keys(d.data