How to Build a Time Tracking Dashboard with Next.js
Time tracking is one of the most requested features in SaaS products. Whether you are building a freelancer platform, a project management tool, or an internal operations dashboard, a time tracker is almost always on the feature list.
In this guide, we will build a production-ready time tracking dashboard using Next.js, Prisma, and PostgreSQL. By the end, you'll have a solid foundation that supports start/stop timers, manual time entry, project-based tracking, and basic reporting.
Why Build Your Own Time Tracker?
Off-the-shelf time tracking tools like Toggl and Harvest are great, but they don't integrate with your specific workflow. Building your own means you can:
- Embed time tracking directly into your existing app
- Customize the data model for your business logic
- Keep all data within your infrastructure
- Add team-specific features without waiting for a vendor update
Setting Up the Database Schema
The heart of any time tracker is the database. Here is a Prisma schema that covers the essentials:
model Project {
id String @id @default(cuid())
name String
color String @default("#6366f1")
billable Boolean @default(true)
rate Decimal? @default(0)
entries TimeEntry[]
createdAt DateTime @default(now())
}
model TimeEntry {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id])
userId String
startTime DateTime
endTime DateTime?
duration Int?
notes String?
billable Boolean @default(true)
createdAt DateTime @default(now())
}Building the Timer Component
The core timer component needs to handle start, stop, and real-time display. Here is a simplified version:
'use client'
import { useState, useEffect, useCallback } from 'react'
export function Timer({ projectId, onEntry }: { projectId: string; onEntry: (entry: any) => void }) {
const [running, setRunning] = useState(false)
const [elapsed, setElapsed] = useState(0)
const [startTime, setStartTime] = useState<Date | null>(null)
useEffect(() => {
if (!running) return
const interval = setInterval(() => {
setElapsed(prev => prev + 1)
}, 1000)
return () => clearInterval(interval)
}, [running])
const handleStart = useCallback(async () => {
const now = new Date()
setStartTime(now)
setRunning(true)
setElapsed(0)
}, [projectId])
const handleStop = useCallback(async () => {
setRunning(false)
const entry = {
projectId,
startTime: startTime!.toISOString(),
endTime: new Date().toISOString(),
duration: elapsed,
}
onEntry(entry)
}, [projectId, startTime, elapsed, onEntry])
return (
<div className="flex items-center gap-4 rounded-xl bg-gray-900 p-4">
<div className="text-3xl font-mono font-bold text-white">
{String(Math.floor(elapsed / 3600)).padStart(2, '0')}:
{String(Math.floor((elapsed % 3600) / 60)).padStart(2, '0')}:
{String(elapsed % 60).padStart(2, '0')}
</div>
<button
onClick={running ? handleStop : handleStart}
className="rounded-lg px-4 py-2 text-sm font-semibold text-white bg-brand-600 hover:bg-brand-700"
>
{running ? 'Stop' : 'Start'}
</button>
</div>
)
}Building the Timesheet View
Once you have time entries, you need a way to display and manage them. A timesheet view should show entries grouped by day with running totals:
export function Timesheet({ entries }: { entries: TimeEntry[] }) {
const grouped = groupBy(entries, e => format(e.startTime, 'yyyy-MM-dd'))
return (
<div className="space-y-6">
{Object.entries(grouped).map(([date, dayEntries]) => (
<div key={date} className="rounded-xl bg-gray-900 p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-white">{format(new Date(date), 'EEEE, MMM d')}</h3>
<span className="text-sm text-gray-400">
{formatDuration(dayEntries.reduce((sum, e) => sum + (e.duration || 0), 0))}
</span>
</div>
{dayEntries.map(entry => (
<div key={entry.id} className="flex items-center justify-between py-2 border-b border-gray-800 last:border-0">
<div>
<p className="text-sm text-white">{entry.project.name}</p>
{entry.notes && <p className="text-xs text-gray-400">{entry.notes}</p>}
</div>
<span className="text-sm text-gray-300">{formatDuration(entry.duration || 0)}</span>
</div>
))}
</div>
))}
</div>
)
}Adding Reports
Reporting transforms raw time data into actionable insights. A weekly summary with billable vs non-billable hours, project breakdowns, and export functionality makes the tool enterprise-ready.
Cross-Selling: Supercharge Your Time Tracker
Our Time Tracker Pro template includes all of this and more — start/stop timer, manual entry, timesheet reporting, project management, team dashboards, and CSV export. It is a complete, production-ready implementation that you can customize and deploy in hours, not weeks.
Pair it with our OKR Goal Tracker to connect time tracking to company objectives, or use Freelancer Finance Hub to turn tracked hours into invoices automatically. The combination creates a powerful business operations suite.
Browse the full collection of templates to find everything you need for your next project.
Related Template
Try this production-ready starter kit to build your project faster.
Time Tracker Pro
Track time, boost productivity
Ready to Build?
Get started with our production-ready starter kits and ship your project faster.
Browse Starter Kits