# How to Build a Productivity App with Lovable

- Tool: How to Build with Lovable
- Difficulty: Intermediate
- Compatibility: Lovable Pro or higher
- Last updated: April 2026

## TL;DR

Build a unified productivity workspace in Lovable combining tasks, notes, time tracking, and a daily planner in one app. Features a Pomodoro timer that logs focus sessions to a time_entries table, cross-referenced notes and tasks, and Tabs-based navigation between modes — all backed by Supabase.

## Before you start

- Lovable Pro account for multi-mode app generation
- Supabase project with URL and anon key saved to Cloud tab → Secrets
- Basic familiarity with the Pomodoro technique (25-minute focus blocks)
- Optional: a list of your projects to set up in the app

## Step-by-step guide

### 1. Set up the schema with tasks, notes, and time tracking

Prompt Lovable to create the complete database schema. The FK relationships between tasks, notes, and time entries are the core of the cross-reference system.

```
Build a unified productivity app. Create these Supabase tables:

- projects: id, user_id, name, color (hex), description, is_archived (bool default false), created_at
- tasks: id, user_id, project_id (FK projects nullable), title, description, status (todo|in_progress|done), priority (low|medium|high|urgent), due_date (date nullable), completed_at (timestamptz nullable), created_at
- notes: id, user_id, task_id (FK tasks nullable), project_id (FK projects nullable), title, content (text), tags (text array), created_at, updated_at
- time_entries: id, user_id, task_id (FK tasks nullable), project_id (FK projects nullable), session_type (work|short_break|long_break), duration_minutes (int), started_at (timestamptz), completed_at (timestamptz)
- daily_plan: id, user_id, task_id (FK tasks), plan_date (date), time_slot (int, hour 0-23), created_at, UNIQUE(user_id, task_id, plan_date)

Create a view weekly_time_summary: user_id, project_id, project_name, week_start (date, Monday), total_minutes — groups time_entries by user+project+week.

RLS: all tables require user_id = auth.uid() for all operations. Enable Realtime on tasks and time_entries.
```

> Pro tip: Ask Lovable to create an inbox project automatically for new users — a special project called 'Inbox' where unassigned tasks go. Use a Supabase trigger on auth.users INSERT to create this default project.

**Expected result:** All five tables and the weekly_time_summary view are created with RLS. The app loads with a Tabs navigation bar at the top showing Tasks, Notes, Timer, and Planner tabs.

### 2. Build the task management mode

Create the Tasks tab with a Kanban-style or list view, project filter, and quick-add functionality. Tasks are the central entity that notes and time entries reference.

```
Build the Tasks tab content at src/components/productivity/TasksMode.tsx:

1. Layout: sidebar with project list (Cards, each showing project color + name + task count), main area with task list
2. Task list:
   - Group by status: To Do, In Progress, Done (three collapsible sections)
   - Each task row: checkbox (clicking sets status=done and completed_at=now), priority Badge (low=gray, medium=blue, high=orange, urgent=red), title, project Badge, due date (red if overdue), a Timer Button that opens the Pomodoro timer pre-linked to this task
   - Inline edit: clicking the title opens an edit Popover with all task fields
3. Quick-add Input at top: type task title and press Enter to create a task in the selected project with status=todo
4. Filter bar: status Select, priority Select, project Select, due date range
5. Task detail Sheet (slides in from right when clicking a task): shows full description, linked notes count, total time logged, edit all fields
6. Sort options: due date, priority, created date, manual drag (update sort_order column)
```

> Pro tip: Ask Lovable to add a 'Focus Mode' button that hides all UI except the current task title and the Pomodoro timer. A simple full-screen div with a blurred background works well for this.

**Expected result:** The Tasks tab shows tasks grouped by status. Checking a task marks it done with a strikethrough. The quick-add Input creates a new task instantly.

### 3. Build the Pomodoro timer with session logging

Create the Timer tab with a circular countdown display, interval controls, and automatic session logging to time_entries. The timer state lives in React but completed sessions are persisted.

```
// src/components/productivity/PomodoroTimer.tsx
import { useState, useEffect, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { supabase } from '@/integrations/supabase/client'
import { useAuth } from '@/hooks/useAuth'

const SESSIONS = {
  work: 25 * 60,
  short_break: 5 * 60,
  long_break: 15 * 60,
} as const

type SessionType = keyof typeof SESSIONS

interface Props {
  defaultTaskId?: string
}

export function PomodoroTimer({ defaultTaskId }: Props) {
  const { user } = useAuth()
  const [sessionType, setSessionType] = useState<SessionType>('work')
  const [timeLeft, setTimeLeft] = useState(SESSIONS.work)
  const [isRunning, setIsRunning] = useState(false)
  const [startedAt, setStartedAt] = useState<string | null>(null)
  const [selectedTaskId, setSelectedTaskId] = useState(defaultTaskId ?? null)

  useEffect(() => {
    setTimeLeft(SESSIONS[sessionType])
    setIsRunning(false)
  }, [sessionType])

  const logSession = useCallback(async () => {
    if (!user || !startedAt) return
    await supabase.from('time_entries').insert({
      user_id: user.id,
      task_id: selectedTaskId,
      session_type: sessionType,
      duration_minutes: SESSIONS[sessionType] / 60,
      started_at: startedAt,
      completed_at: new Date().toISOString(),
    })
  }, [user, startedAt, selectedTaskId, sessionType])

  useEffect(() => {
    if (!isRunning) return
    if (timeLeft === 0) {
      setIsRunning(false)
      logSession()
      return
    }
    const interval = setInterval(() => setTimeLeft((t) => t - 1), 1000)
    return () => clearInterval(interval)
  }, [isRunning, timeLeft, logSession])

  const minutes = Math.floor(timeLeft / 60).toString().padStart(2, '0')
  const seconds = (timeLeft % 60).toString().padStart(2, '0')
  const progress = ((SESSIONS[sessionType] - timeLeft) / SESSIONS[sessionType]) * 100

  return (
    <div className="flex flex-col items-center gap-6 p-8">
      <div className="relative w-48 h-48">
        <svg className="w-full h-full -rotate-90" viewBox="0 0 100 100">
          <circle cx="50" cy="50" r="45" fill="none" stroke="currentColor" strokeWidth="4" className="text-muted" />
          <circle cx="50" cy="50" r="45" fill="none" stroke="currentColor" strokeWidth="4"
            className="text-primary transition-all duration-1000"
            strokeDasharray={`${2 * Math.PI * 45}`}
            strokeDashoffset={`${2 * Math.PI * 45 * (1 - progress / 100)}`} />
        </svg>
        <div className="absolute inset-0 flex items-center justify-center">
          <span className="text-4xl font-mono font-bold">{minutes}:{seconds}</span>
        </div>
      </div>
      <div className="flex gap-2">
        {(['work', 'short_break', 'long_break'] as SessionType[]).map((s) => (
          <Button key={s} variant={sessionType === s ? 'default' : 'outline'} size="sm" onClick={() => setSessionType(s)}>
            {s === 'work' ? 'Focus' : s === 'short_break' ? 'Short Break' : 'Long Break'}
          </Button>
        ))}
      </div>
      <Button size="lg" onClick={() => { if (!isRunning) setStartedAt(new Date().toISOString()); setIsRunning(!isRunning) }}>
        {isRunning ? 'Pause' : timeLeft === SESSIONS[sessionType] ? 'Start' : 'Resume'}
      </Button>
    </div>
  )
}
```

**Expected result:** The Timer tab shows the circular countdown. Starting and completing a 25-minute work session inserts a row into time_entries. The session type buttons switch between Focus, Short Break, and Long Break.

### 4. Build the daily planner view

Create the Planner tab showing today's tasks in a time block grid. Users drag tasks into time slots and the assignments persist in the daily_plan table.

```
Build the Planner tab at src/components/productivity/DailyPlanner.tsx:

1. Header: today's date in large text, day navigation (< and > arrows to change the plan_date)
2. Left column: 'Unscheduled Today' — fetches tasks WHERE due_date = plan_date AND task not in daily_plan for this date. Shows as a scrollable list of draggable task chips.
3. Right area: time grid from 6 AM to 10 PM in 1-hour slots
   - Each slot is a drop zone. Dropping a task chip on a slot calls supabase.from('daily_plan').upsert({ user_id, task_id, plan_date, time_slot: hour })
   - Slots that have tasks show the task title as a colored chip (using project color)
   - Clicking a chip in the grid removes it from the slot (deletes from daily_plan)
4. Drag and drop: use HTML5 drag and drop API (draggable attribute + onDragStart/onDrop handlers) — no external library needed
5. 'Schedule remaining' button: assigns all unscheduled tasks to the next available slot starting from current hour
6. Show current time indicator: a red horizontal line on the current hour slot
```

**Expected result:** The Planner tab shows a time grid. Dragging a task from the unscheduled list to a time slot creates a daily_plan entry. Refreshing the page preserves all scheduled tasks.

### 5. Add the notes mode with task cross-references

Build the Notes tab with a note editor, task linking, tag filtering, and full-text search using Supabase's tsvector search on the notes content.

```
Build the Notes tab at src/components/productivity/NotesMode.tsx:

1. Three-panel layout (desktop): note list (left sidebar), note editor (center), linked items (right panel)
2. Note list:
   - Search Input that filters via Supabase full-text search: .textSearch('content', searchTerm)
   - Tag filter pills using Badge components
   - Each list item: note title (first line of content), updated_at relative time, linked task count Badge
   - 'New Note' Button at top creates a blank note
3. Note editor:
   - Title Input (auto-saves on blur)
   - Textarea for content (auto-saves 2 seconds after last keystroke using debounce)
   - Tags Input: type a tag and press Enter to add, click x on Badge to remove
4. Linked items panel:
   - 'Link to Task' Combobox: search tasks and select one to link (sets task_id on this note)
   - 'Link to Project' Select: sets project_id on this note
   - Shows linked task title with a link to open the task in the Tasks tab
5. Auto-save indicator: shows 'Saving...' then 'Saved' in the top-right of the editor
```

**Expected result:** The Notes tab shows a note list, editor, and linked items panel. Creating a note, typing content, and linking it to a task all work. Search filters notes by content in real time.

## Complete code example

File: `src/hooks/useWeeklyTime.ts`

```typescript
import { useQuery } from '@tanstack/react-query'
import { supabase } from '@/integrations/supabase/client'
import { format, startOfWeek } from 'date-fns'

export interface WeeklyTimeSummary {
  project_id: string | null
  project_name: string
  week_start: string
  total_minutes: number
  total_hours: number
}

export function useWeeklyTime(userId: string, weeksBack = 8) {
  return useQuery({
    queryKey: ['weekly-time', userId, weeksBack],
    queryFn: async () => {
      const since = format(
        startOfWeek(new Date(Date.now() - weeksBack * 7 * 86400000), { weekStartsOn: 1 }),
        'yyyy-MM-dd'
      )
      const { data, error } = await supabase
        .from('weekly_time_summary')
        .select('project_id, project_name, week_start, total_minutes')
        .eq('user_id', userId)
        .gte('week_start', since)
        .order('week_start', { ascending: true })

      if (error) throw error

      return (data ?? []).map((row) => ({
        ...row,
        total_hours: Math.round((row.total_minutes / 60) * 10) / 10,
        week_label: format(new Date(row.week_start), 'MMM d'),
      }))
    },
    staleTime: 60_000,
  })
}
```

## Common mistakes

- **Storing timer state in Supabase on every tick** — A Pomodoro timer counts down every second. Inserting a database row every second creates 1,500 writes per session, exhausting Supabase free tier limits and causing unnecessary lag. Fix: Keep the timer countdown in React state only. Only write to the database once when a session completes (timeLeft reaches 0) or when the user manually stops. The time_entries INSERT happens at completion, not during the countdown.
- **Not enabling Realtime on the tasks table** — If you have the daily planner and task list open in different browser tabs, changes in one tab don't appear in the other without a page refresh. Fix: Enable Supabase Realtime on the tasks and daily_plan tables. Subscribe to changes using supabase.channel('tasks').on('postgres_changes', ...) in the relevant components. Lovable sets this up automatically if you ask for real-time updates in your prompt.
- **Linking notes only to tasks and forgetting project context** — Users often want notes attached to a project but not to any specific task — like meeting notes or project planning docs. Without a project_id FK on notes, these notes are orphaned. Fix: Allow notes to have either task_id, project_id, or both. The schema in Step 1 includes both FKs as nullable. The UI should allow creating notes with just a project link, just a task link, or both.
- **Not debouncing note auto-save** — Saving on every keystroke creates a database write for every character typed. With fast typing, this causes hundreds of writes per minute and can hit Supabase rate limits. Fix: Debounce the auto-save with a 2-second delay. Only save after the user stops typing for 2 seconds. Use the useDebounce hook from usehooks-ts or implement a simple setTimeout-based debounce in the note editor component.

## Best practices

- Use optimistic updates for task completion toggling. Set the task status locally before the Supabase UPDATE completes so the UI responds instantly. Revert on error.
- Keep the Pomodoro timer's running state in React, not in Supabase. Only persist completed sessions. This avoids unnecessary database writes and keeps the timer responsive.
- Add a soft delete (is_deleted bool) to tasks instead of hard deleting. Show a trash/archive view. Users frequently delete tasks by mistake and want to recover them.
- Use the time_entries table as an immutable append-only log. Never update or delete time entries — only insert. This creates a reliable audit trail of all work sessions.
- Index the due_date column on the tasks table since the daily planner queries WHERE due_date = today on every page load. Add: CREATE INDEX idx_tasks_user_due ON tasks(user_id, due_date).
- Show empty states that guide users toward action. An empty task list should say 'No tasks yet — press N to add your first task' rather than showing nothing.

## Frequently asked questions

### Can I use this on mobile to log Pomodoro sessions throughout the day?

Yes. Lovable apps are responsive by default and work in mobile browsers. The Pomodoro timer uses React state with setInterval, which works in mobile browsers. The circular SVG progress indicator renders correctly on all screen sizes. For a native mobile experience, you'd need to export the code and build a React Native version separately.

### What happens to my timer if I accidentally close the browser tab?

Timer state is in React memory only and is lost on page close. This is by design — only completed sessions are logged, not interrupted ones. If you need persistence across tab closes, ask Lovable to add localStorage.setItem calls to save the timer start time and session type. On component mount, check localStorage and resume the timer from where it left off.

### Can notes have rich text formatting like bold and bullet points?

The base build uses a plain Textarea for notes. To add rich text formatting, ask Lovable to integrate a Tiptap editor — it's a React-based rich text editor that generates clean HTML or Markdown. Store the formatted content as a string in the notes.content column. Tiptap is compatible with Lovable's Vite+React setup.

### How do I set up recurring daily tasks that automatically appear in my planner?

Add a recurring_tasks table similar to the finance tracker's recurring_transactions. A Supabase Edge Function scheduled with pg_cron runs each morning and creates today's task instances from recurring templates. Link the task generator to your daily planner so recurring tasks appear automatically in the unscheduled column.

### Is there a way to sync tasks with Google Calendar or Notion?

Not out of the box, but you can add integrations via Edge Functions. For Google Calendar, create an OAuth connection that pushes tasks with due dates as calendar events when the due date is set. For Notion, use the Notion API from an Edge Function to pull pages as notes. Ask Lovable to add a 'Connect Integrations' settings page after completing the base build.

### How do I share my daily plan with a colleague or manager?

Add a 'Share Daily Plan' button that generates a public read-only URL. Store a share_token (UUID) in the daily_plan table per date per user. Create a public Supabase view that allows anonymous SELECT on daily_plan rows where share_token matches. The public URL shows a read-only version of the planner for that day.

### Can multiple people use this as a shared workspace?

The base build is single-user. To add team functionality, you'd need to add a workspaces table, invite system, and update all RLS policies. This is the 'Shared workspaces' customization idea in this guide — it's a significant extension. Prompt Lovable to add the workspace feature as a separate step after the base build is working.

### How do I export my time tracking data to a spreadsheet?

Ask Lovable to add a CSV export button on the time tracking page. Query all time_entries for the selected date range, format them as CSV rows (date, project, task, duration, session type), and trigger a browser download using a Blob URL. This is a pure frontend feature that requires no backend changes.

---

Source: https://www.rapidevelopers.com/how-to-build-lovable/productivity-app
© RapidDev — https://www.rapidevelopers.com/how-to-build-lovable/productivity-app
