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.
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