time-tracker / index.html
festplatten's picture
its broken - please debug the complete project - Follow Up Deployment
3bc5750 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minimal Time Tracker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<script src="https://unpkg.com/feather-icons"></script>
<style>
.timer-card {
transition: all 0.3s ease;
}
.timer-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
}
.time-display {
font-family: 'SF Mono', monospace;
font-size: 3rem;
font-weight: 300;
display: flex;
gap: 0.25rem;
}
.time-segment {
background: #f3f4f6;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
position: relative;
overflow: hidden;
}
.time-segment::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: rgba(255,255,255,0.2);
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.sheet-row {
border-bottom: 1px solid #e5e7eb;
}
.sheet-row:last-child {
border-bottom: none;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- Header -->
<header class="mb-12">
<h1 class="text-4xl font-light text-gray-900 text-center mb-2">Time Tracker</h1>
<p class="text-gray-500 text-center max-w-lg mx-auto">Track time across multiple projects with minimal distraction</p>
</header>
<!-- Timer Creation Form -->
<div class="bg-white rounded-xl shadow-sm p-6 mb-8" data-aos="fade-up">
<h2 class="text-xl font-medium text-gray-800 mb-4">Create New Timer</h2>
<form id="timer-form" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label for="client" class="block text-sm font-medium text-gray-700 mb-1">Client</label>
<input type="text" id="client" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="project" class="block text-sm font-medium text-gray-700 mb-1">Project</label>
<input type="text" id="project" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div>
<label for="task" class="block text-sm font-medium text-gray-700 mb-1">Task</label>
<input type="text" id="task" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
<div class="flex items-end">
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-md transition duration-150 ease-in-out flex items-center justify-center">
<i data-feather="plus" class="mr-2"></i> Start Timer
</button>
</div>
</form>
</div>
<!-- Active Timers Section -->
<section class="mb-12">
<h2 class="text-xl font-medium text-gray-800 mb-4">Active Timers</h2>
<div id="active-timers" class="grid grid-cols-1 gap-4">
<!-- Timers will be added here dynamically -->
</div>
</section>
<!-- Time Sheet Section -->
<section>
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-medium text-gray-800">Time Sheet</h2>
<div class="flex space-x-2">
<button id="clear-sheet" class="text-sm text-gray-500 hover:text-gray-700 flex items-center">
<i data-feather="trash-2" class="w-4 h-4 mr-1"></i> Clear All
</button>
<button id="daily-close" class="text-sm text-red-500 hover:text-red-700 flex items-center">
<i data-feather="file-text" class="w-4 h-4 mr-1"></i> Daily Closing
</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<div class="grid grid-cols-12 bg-gray-50 p-4 border-b border-gray-200">
<div class="col-span-3 text-sm font-medium text-gray-500">Client</div>
<div class="col-span-3 text-sm font-medium text-gray-500">Project</div>
<div class="col-span-3 text-sm font-medium text-gray-500">Task</div>
<div class="col-span-2 text-sm font-medium text-gray-500">Duration</div>
<div class="col-span-1 text-sm font-medium text-gray-500">Date</div>
</div>
<div id="time-sheet" class="divide-y divide-gray-200">
<!-- Time entries will be added here dynamically -->
<div class="grid grid-cols-12 p-4 items-center sheet-row">
<div class="col-span-3 text-gray-700">Example Client</div>
<div class="col-span-3 text-gray-700">Website Redesign</div>
<div class="col-span-3 text-gray-700">Homepage Layout</div>
<div class="col-span-2 text-gray-700">01:23:45</div>
<div class="col-span-1 text-sm text-gray-500">Today</div>
</div>
</div>
</div>
</section>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
AOS.init();
feather.replace();
const timerForm = document.getElementById('timer-form');
const activeTimersContainer = document.getElementById('active-timers');
const timeSheetContainer = document.getElementById('time-sheet');
const clearSheetBtn = document.getElementById('clear-sheet');
let timers = [];
let timeEntries = JSON.parse(localStorage.getItem('timeEntries')) || [];
// Render initial time sheet if entries exist
if (timeEntries.length > 0) {
renderTimeSheet();
} else {
// Remove example entry if no saved entries
timeSheetContainer.innerHTML = '';
}
timerForm.addEventListener('submit', function(e) {
e.preventDefault();
const client = document.getElementById('client').value;
const project = document.getElementById('project').value;
const task = document.getElementById('task').value;
if (!client || !project || !task) return;
const timerId = Date.now().toString();
const newTimer = {
id: timerId,
client,
project,
task,
startTime: new Date(),
isRunning: true,
elapsed: 0
};
timers.push(newTimer);
renderActiveTimers();
// Clear form
timerForm.reset();
});
document.getElementById('daily-close').addEventListener('click', generateDailyReport);
clearSheetBtn.addEventListener('click', function() {
if (timeEntries.length === 0) return;
if (confirm('Are you sure you want to clear all time entries?')) {
timeEntries = [];
localStorage.setItem('timeEntries', JSON.stringify(timeEntries));
timeSheetContainer.innerHTML = '';
}
});
function renderActiveTimers() {
activeTimersContainer.innerHTML = '';
timers.forEach(timer => {
const timerCard = document.createElement('div');
timerCard.className = 'timer-card bg-white rounded-xl shadow-sm p-6 flex flex-col md:flex-row md:items-center justify-between';
const timerInfo = document.createElement('div');
timerInfo.className = 'mb-4 md:mb-0';
const timerTitle = document.createElement('h3');
timerTitle.className = 'text-lg font-medium text-gray-800';
timerTitle.textContent = `${timer.client} / ${timer.project} / ${timer.task}`;
const timerTime = document.createElement('div');
timerTime.className = 'time-display text-gray-700 mt-2';
const formattedTime = formatTime(timer.elapsed).split(':');
['hours', 'minutes', 'seconds'].forEach((unit, index) => {
const segment = document.createElement('div');
segment.className = 'flex flex-col items-center';
const label = document.createElement('div');
label.className = 'text-xs text-gray-500 uppercase mt-1';
label.textContent = unit;
const value = document.createElement('div');
value.className = 'time-segment';
value.textContent = formattedTime[index];
segment.appendChild(value);
segment.appendChild(label);
timerTime.appendChild(segment);
});
timerInfo.appendChild(timerTitle);
timerInfo.appendChild(timerTime);
const timerActions = document.createElement('div');
timerActions.className = 'flex space-x-2';
const pauseBtn = document.createElement('button');
pauseBtn.className = `px-4 py-2 rounded-md flex items-center ${timer.isRunning ? 'bg-yellow-500 hover:bg-yellow-600' : 'bg-blue-600 hover:bg-blue-700'} text-white transition duration-150 ease-in-out`;
pauseBtn.innerHTML = `<i data-feather="${timer.isRunning ? 'pause' : 'play'}" class="mr-2"></i> ${timer.isRunning ? 'Pause' : 'Resume'}`;
const stopBtn = document.createElement('button');
stopBtn.className = 'px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition duration-150 ease-in-out flex items-center';
stopBtn.innerHTML = '<i data-feather="square" class="mr-2"></i> Stop';
timerActions.appendChild(pauseBtn);
timerActions.appendChild(stopBtn);
timerCard.appendChild(timerInfo);
timerCard.appendChild(timerActions);
activeTimersContainer.appendChild(timerCard);
// Add event listeners
pauseBtn.addEventListener('click', () => toggleTimer(timer.id));
stopBtn.addEventListener('click', () => stopTimer(timer.id));
// Start interval for running timers
if (timer.isRunning) {
const intervalId = setInterval(() => {
timer.elapsed += 1;
timerTime.textContent = formatTime(timer.elapsed);
}, 1000);
timer.intervalId = intervalId;
}
feather.replace();
});
}
function toggleTimer(timerId) {
const timer = timers.find(t => t.id === timerId);
if (!timer) return;
timer.isRunning = !timer.isRunning;
if (timer.isRunning) {
const intervalId = setInterval(() => {
timer.elapsed += 1;
const timerElement = document.querySelector(`[data-timer-id="${timerId}"]`);
if (timerElement) {
const formattedTime = formatTime(timer.elapsed).split(':');
const segments = timerElement.parentElement.querySelectorAll('.time-segment');
segments.forEach((segment, index) => {
segment.textContent = formattedTime[index];
});
}
}, 1000);
timer.intervalId = intervalId;
} else {
clearInterval(timer.intervalId);
}
renderActiveTimers();
}
function stopTimer(timerId) {
const timerIndex = timers.findIndex(t => t.id === timerId);
if (timerIndex === -1) return;
const timer = timers[timerIndex];
// Clear interval
if (timer.intervalId) {
clearInterval(timer.intervalId);
}
// Add to time sheet
const now = new Date();
const dateString = now.toLocaleDateString();
const timeEntry = {
client: timer.client,
project: timer.project,
task: timer.task,
duration: timer.elapsed,
date: dateString,
timestamp: now.getTime()
};
timeEntries.push(timeEntry);
localStorage.setItem('timeEntries', JSON.stringify(timeEntries));
// Remove from active timers
timers.splice(timerIndex, 1);
renderActiveTimers();
renderTimeSheet();
}
function generateDailyReport() {
if (timeEntries.length === 0) {
alert('No time entries to generate report');
return;
}
// Group by client
const clients = {};
timeEntries.forEach(entry => {
const clientKey = entry.client.substring(0, 3).toUpperCase();
if (!clients[clientKey]) {
clients[clientKey] = [];
}
clients[clientKey].push(entry);
});
// Generate report text
let reportText = '';
let grandTotalSeconds = 0;
Object.entries(clients).forEach(([clientKey, entries]) => {
reportText += `${clientKey}\n-----\n`;
let clientTotalSeconds = 0;
entries.forEach(entry => {
reportText += `${entry.client} / ${entry.project} / ${entry.task} / ${formatTime(entry.duration)} / ${entry.date}\n`;
clientTotalSeconds += entry.duration;
});
grandTotalSeconds += clientTotalSeconds;
reportText += `_____\nTotal Time: ${formatTime(clientTotalSeconds)}\n-----\n`;
});
reportText += `\nGRAND TOTAL: ${formatTime(grandTotalSeconds)}`;
// Create download
const blob = new Blob([reportText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `time_report_${new Date().toISOString().split('T')[0]}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
function renderTimeSheet() {
timeSheetContainer.innerHTML = '';
timeEntries.sort((a, b) => b.timestamp - a.timestamp).forEach(entry => {
const entryRow = document.createElement('div');
entryRow.className = 'grid grid-cols-12 p-4 items-center sheet-row';
const clientCell = document.createElement('div');
clientCell.className = 'col-span-3 text-gray-700';
clientCell.textContent = entry.client;
const projectCell = document.createElement('div');
projectCell.className = 'col-span-3 text-gray-700';
projectCell.textContent = entry.project;
const taskCell = document.createElement('div');
taskCell.className = 'col-span-3 text-gray-700';
taskCell.textContent = entry.task;
const durationCell = document.createElement('div');
durationCell.className = 'col-span-2 text-gray-700';
durationCell.textContent = formatTime(entry.duration);
const dateCell = document.createElement('div');
dateCell.className = 'col-span-1 text-sm text-gray-500';
dateCell.textContent = entry.date === new Date().toLocaleDateString() ? 'Today' : entry.date;
entryRow.appendChild(clientCell);
entryRow.appendChild(projectCell);
entryRow.appendChild(taskCell);
entryRow.appendChild(durationCell);
entryRow.appendChild(dateCell);
timeSheetContainer.appendChild(entryRow);
});
}
function formatTime(seconds) {
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return [
hrs.toString().padStart(2, '0'),
mins.toString().padStart(2, '0'),
secs.toString().padStart(2, '0')
].join(':');
}
});
</script>
</body>
</html>