Existing ASP.NET API with vanilla JS SPA, WindowWatcher, Chrome extension, and MCP server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
966 lines
29 KiB
Markdown
966 lines
29 KiB
Markdown
# TaskTracker Web UI Redesign — Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Replace the vanilla JS SPA with a React Kanban board app featuring rich analytics, subtask management, time estimates, and search/filters.
|
|
|
|
**Architecture:** Separate Vite+React project (`TaskTracker.Web`) calling the existing ASP.NET API at `localhost:5200`. Two small API changes needed first (time estimate field, task update endpoint). Frontend uses TanStack Query for server state, dnd-kit for drag-and-drop, Recharts for analytics.
|
|
|
|
**Tech Stack:** React 19, TypeScript, Vite, Tailwind CSS, @dnd-kit, @tanstack/react-query, react-router-dom, Recharts, Axios, lucide-react
|
|
|
|
**Design doc:** `docs/plans/2026-02-26-web-ui-redesign-design.md`
|
|
|
|
---
|
|
|
|
## Task 1: API — Add EstimatedMinutes field and Update endpoint
|
|
|
|
The React UI needs two things the API doesn't have yet: a time estimate field on tasks, and a general-purpose update endpoint for inline editing.
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Core/Entities/WorkTask.cs`
|
|
- Modify: `TaskTracker.Core/DTOs/CreateTaskRequest.cs`
|
|
- Create: `TaskTracker.Core/DTOs/UpdateTaskRequest.cs`
|
|
- Modify: `TaskTracker.Api/Controllers/TasksController.cs`
|
|
- Modify: `TaskTracker.Infrastructure/Data/AppDbContext.cs` (if migration needed)
|
|
|
|
**Step 1: Add EstimatedMinutes to WorkTask entity**
|
|
|
|
In `TaskTracker.Core/Entities/WorkTask.cs`, add:
|
|
```csharp
|
|
public int? EstimatedMinutes { get; set; }
|
|
```
|
|
|
|
**Step 2: Add EstimatedMinutes to CreateTaskRequest**
|
|
|
|
In `TaskTracker.Core/DTOs/CreateTaskRequest.cs`, add:
|
|
```csharp
|
|
public int? EstimatedMinutes { get; set; }
|
|
```
|
|
|
|
**Step 3: Create UpdateTaskRequest DTO**
|
|
|
|
Create `TaskTracker.Core/DTOs/UpdateTaskRequest.cs`:
|
|
```csharp
|
|
namespace TaskTracker.Core.DTOs;
|
|
|
|
public class UpdateTaskRequest
|
|
{
|
|
public string? Title { get; set; }
|
|
public string? Description { get; set; }
|
|
public string? Category { get; set; }
|
|
public int? EstimatedMinutes { get; set; }
|
|
}
|
|
```
|
|
|
|
**Step 4: Wire EstimatedMinutes in TasksController.Create**
|
|
|
|
In `TasksController.cs`, update the `Create` method's `new WorkTask` block to include:
|
|
```csharp
|
|
EstimatedMinutes = request.EstimatedMinutes,
|
|
```
|
|
|
|
**Step 5: Add PUT update endpoint to TasksController**
|
|
|
|
Add to `TasksController.cs`:
|
|
```csharp
|
|
[HttpPut("{id:int}")]
|
|
public async Task<IActionResult> Update(int id, [FromBody] UpdateTaskRequest request)
|
|
{
|
|
var task = await taskRepo.GetByIdAsync(id);
|
|
if (task is null)
|
|
return NotFound(ApiResponse.Fail("Task not found"));
|
|
|
|
if (request.Title is not null) task.Title = request.Title;
|
|
if (request.Description is not null) task.Description = request.Description;
|
|
if (request.Category is not null) task.Category = request.Category;
|
|
if (request.EstimatedMinutes.HasValue) task.EstimatedMinutes = request.EstimatedMinutes;
|
|
|
|
await taskRepo.UpdateAsync(task);
|
|
return Ok(ApiResponse<WorkTask>.Ok(task));
|
|
}
|
|
```
|
|
|
|
**Step 6: Create and apply EF migration**
|
|
|
|
Run:
|
|
```bash
|
|
cd TaskTracker.Infrastructure
|
|
dotnet ef migrations add AddEstimatedMinutes --startup-project ../TaskTracker.Api
|
|
dotnet ef database update --startup-project ../TaskTracker.Api
|
|
```
|
|
|
|
**Step 7: Verify with Swagger**
|
|
|
|
Run the API (`dotnet run --project TaskTracker.Api`) and test the new PUT endpoint at `/swagger`.
|
|
|
|
**Step 8: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Core/Entities/WorkTask.cs TaskTracker.Core/DTOs/CreateTaskRequest.cs TaskTracker.Core/DTOs/UpdateTaskRequest.cs TaskTracker.Api/Controllers/TasksController.cs TaskTracker.Infrastructure/
|
|
git commit -m "feat: add EstimatedMinutes field and general PUT update endpoint for tasks"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Scaffold React project with Vite + Tailwind
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/` (entire project scaffold)
|
|
|
|
**Step 1: Create Vite React TypeScript project**
|
|
|
|
```bash
|
|
cd C:/Users/AJ/Desktop/Projects/TaskTracker
|
|
npm create vite@latest TaskTracker.Web -- --template react-ts
|
|
```
|
|
|
|
**Step 2: Install dependencies**
|
|
|
|
```bash
|
|
cd TaskTracker.Web
|
|
npm install axios @tanstack/react-query react-router-dom @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities recharts lucide-react
|
|
npm install -D tailwindcss @tailwindcss/vite
|
|
```
|
|
|
|
**Step 3: Configure Tailwind**
|
|
|
|
Replace `TaskTracker.Web/src/index.css` with:
|
|
```css
|
|
@import "tailwindcss";
|
|
```
|
|
|
|
Add Tailwind plugin to `TaskTracker.Web/vite.config.ts`:
|
|
```typescript
|
|
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import tailwindcss from '@tailwindcss/vite'
|
|
|
|
export default defineConfig({
|
|
plugins: [react(), tailwindcss()],
|
|
server: {
|
|
port: 5173,
|
|
proxy: {
|
|
'/api': {
|
|
target: 'http://localhost:5200',
|
|
changeOrigin: true,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
```
|
|
|
|
**Step 4: Clean up scaffold**
|
|
|
|
- Delete `src/App.css`, `src/assets/`
|
|
- Replace `src/App.tsx` with a minimal placeholder:
|
|
|
|
```tsx
|
|
function App() {
|
|
return <div className="bg-[#0f1117] min-h-screen text-white p-8">
|
|
<h1 className="text-2xl font-semibold">TaskTracker</h1>
|
|
</div>
|
|
}
|
|
export default App
|
|
```
|
|
|
|
- Replace `src/main.tsx`:
|
|
|
|
```tsx
|
|
import { StrictMode } from 'react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import './index.css'
|
|
import App from './App'
|
|
|
|
createRoot(document.getElementById('root')!).render(
|
|
<StrictMode>
|
|
<App />
|
|
</StrictMode>,
|
|
)
|
|
```
|
|
|
|
**Step 5: Verify it runs**
|
|
|
|
```bash
|
|
npm run dev
|
|
```
|
|
|
|
Open `http://localhost:5173` — should see "TaskTracker" on a dark background.
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/
|
|
git commit -m "feat: scaffold TaskTracker.Web with Vite, React, TypeScript, Tailwind"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: TypeScript types + API client + TanStack Query provider
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/types/index.ts`
|
|
- Create: `TaskTracker.Web/src/api/client.ts`
|
|
- Create: `TaskTracker.Web/src/api/tasks.ts`
|
|
- Create: `TaskTracker.Web/src/api/context.ts`
|
|
- Create: `TaskTracker.Web/src/api/mappings.ts`
|
|
- Modify: `TaskTracker.Web/src/main.tsx`
|
|
|
|
**Step 1: Create TypeScript types matching the API**
|
|
|
|
Create `TaskTracker.Web/src/types/index.ts`:
|
|
```typescript
|
|
export enum WorkTaskStatus {
|
|
Pending = 0,
|
|
Active = 1,
|
|
Paused = 2,
|
|
Completed = 3,
|
|
Abandoned = 4,
|
|
}
|
|
|
|
export enum NoteType {
|
|
PauseNote = 0,
|
|
ResumeNote = 1,
|
|
General = 2,
|
|
}
|
|
|
|
export interface WorkTask {
|
|
id: number
|
|
title: string
|
|
description: string | null
|
|
status: WorkTaskStatus
|
|
category: string | null
|
|
createdAt: string
|
|
startedAt: string | null
|
|
completedAt: string | null
|
|
estimatedMinutes: number | null
|
|
parentTaskId: number | null
|
|
subTasks: WorkTask[]
|
|
notes: TaskNote[]
|
|
contextEvents: ContextEvent[]
|
|
}
|
|
|
|
export interface TaskNote {
|
|
id: number
|
|
workTaskId: number
|
|
content: string
|
|
type: NoteType
|
|
createdAt: string
|
|
}
|
|
|
|
export interface ContextEvent {
|
|
id: number
|
|
workTaskId: number | null
|
|
source: string
|
|
appName: string
|
|
windowTitle: string
|
|
url: string | null
|
|
timestamp: string
|
|
}
|
|
|
|
export interface AppMapping {
|
|
id: number
|
|
pattern: string
|
|
matchType: string
|
|
category: string
|
|
friendlyName: string | null
|
|
}
|
|
|
|
export interface ContextSummaryItem {
|
|
appName: string
|
|
category: string
|
|
eventCount: number
|
|
firstSeen: string
|
|
lastSeen: string
|
|
}
|
|
|
|
export interface ApiResponse<T> {
|
|
success: boolean
|
|
data: T
|
|
error: string | null
|
|
}
|
|
```
|
|
|
|
**Step 2: Create Axios client**
|
|
|
|
Create `TaskTracker.Web/src/api/client.ts`:
|
|
```typescript
|
|
import axios from 'axios'
|
|
import type { ApiResponse } from '../types'
|
|
|
|
const api = axios.create({ baseURL: '/api' })
|
|
|
|
export async function request<T>(config: Parameters<typeof api.request>[0]): Promise<T> {
|
|
const { data } = await api.request<ApiResponse<T>>(config)
|
|
if (!data.success) throw new Error(data.error ?? 'API error')
|
|
return data.data
|
|
}
|
|
|
|
export default api
|
|
```
|
|
|
|
**Step 3: Create task API hooks**
|
|
|
|
Create `TaskTracker.Web/src/api/tasks.ts`:
|
|
```typescript
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { request } from './client'
|
|
import type { WorkTask } from '../types'
|
|
|
|
export function useTasks(includeSubTasks = true) {
|
|
return useQuery({
|
|
queryKey: ['tasks', { includeSubTasks }],
|
|
queryFn: () => request<WorkTask[]>({ url: '/tasks', params: { includeSubTasks } }),
|
|
})
|
|
}
|
|
|
|
export function useActiveTask() {
|
|
return useQuery({
|
|
queryKey: ['tasks', 'active'],
|
|
queryFn: () => request<WorkTask | null>({ url: '/tasks/active' }),
|
|
refetchInterval: 30_000,
|
|
})
|
|
}
|
|
|
|
export function useTask(id: number) {
|
|
return useQuery({
|
|
queryKey: ['tasks', id],
|
|
queryFn: () => request<WorkTask>({ url: `/tasks/${id}` }),
|
|
})
|
|
}
|
|
|
|
function useInvalidateTasks() {
|
|
const qc = useQueryClient()
|
|
return () => {
|
|
qc.invalidateQueries({ queryKey: ['tasks'] })
|
|
}
|
|
}
|
|
|
|
export function useCreateTask() {
|
|
const invalidate = useInvalidateTasks()
|
|
return useMutation({
|
|
mutationFn: (body: { title: string; description?: string; category?: string; parentTaskId?: number; estimatedMinutes?: number }) =>
|
|
request<WorkTask>({ method: 'POST', url: '/tasks', data: body }),
|
|
onSuccess: invalidate,
|
|
})
|
|
}
|
|
|
|
export function useUpdateTask() {
|
|
const invalidate = useInvalidateTasks()
|
|
return useMutation({
|
|
mutationFn: ({ id, ...body }: { id: number; title?: string; description?: string; category?: string; estimatedMinutes?: number }) =>
|
|
request<WorkTask>({ method: 'PUT', url: `/tasks/${id}`, data: body }),
|
|
onSuccess: invalidate,
|
|
})
|
|
}
|
|
|
|
export function useStartTask() {
|
|
const invalidate = useInvalidateTasks()
|
|
return useMutation({
|
|
mutationFn: (id: number) => request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/start` }),
|
|
onSuccess: invalidate,
|
|
})
|
|
}
|
|
|
|
export function usePauseTask() {
|
|
const invalidate = useInvalidateTasks()
|
|
return useMutation({
|
|
mutationFn: ({ id, note }: { id: number; note?: string }) =>
|
|
request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/pause`, data: { note } }),
|
|
onSuccess: invalidate,
|
|
})
|
|
}
|
|
|
|
export function useResumeTask() {
|
|
const invalidate = useInvalidateTasks()
|
|
return useMutation({
|
|
mutationFn: ({ id, note }: { id: number; note?: string }) =>
|
|
request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/resume`, data: { note } }),
|
|
onSuccess: invalidate,
|
|
})
|
|
}
|
|
|
|
export function useCompleteTask() {
|
|
const invalidate = useInvalidateTasks()
|
|
return useMutation({
|
|
mutationFn: (id: number) => request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/complete` }),
|
|
onSuccess: invalidate,
|
|
})
|
|
}
|
|
|
|
export function useAbandonTask() {
|
|
const invalidate = useInvalidateTasks()
|
|
return useMutation({
|
|
mutationFn: (id: number) => request<void>({ method: 'DELETE', url: `/tasks/${id}` }),
|
|
onSuccess: invalidate,
|
|
})
|
|
}
|
|
```
|
|
|
|
**Step 4: Create context API hooks**
|
|
|
|
Create `TaskTracker.Web/src/api/context.ts`:
|
|
```typescript
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { request } from './client'
|
|
import type { ContextEvent, ContextSummaryItem } from '../types'
|
|
|
|
export function useRecentContext(minutes = 30) {
|
|
return useQuery({
|
|
queryKey: ['context', 'recent', minutes],
|
|
queryFn: () => request<ContextEvent[]>({ url: '/context/recent', params: { minutes } }),
|
|
refetchInterval: 60_000,
|
|
})
|
|
}
|
|
|
|
export function useContextSummary() {
|
|
return useQuery({
|
|
queryKey: ['context', 'summary'],
|
|
queryFn: () => request<ContextSummaryItem[]>({ url: '/context/summary' }),
|
|
refetchInterval: 60_000,
|
|
})
|
|
}
|
|
```
|
|
|
|
**Step 5: Create mappings API hooks**
|
|
|
|
Create `TaskTracker.Web/src/api/mappings.ts`:
|
|
```typescript
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { request } from './client'
|
|
import type { AppMapping } from '../types'
|
|
|
|
export function useMappings() {
|
|
return useQuery({
|
|
queryKey: ['mappings'],
|
|
queryFn: () => request<AppMapping[]>({ url: '/mappings' }),
|
|
})
|
|
}
|
|
|
|
export function useCreateMapping() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (body: { pattern: string; matchType: string; category: string; friendlyName?: string }) =>
|
|
request<AppMapping>({ method: 'POST', url: '/mappings', data: body }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['mappings'] }),
|
|
})
|
|
}
|
|
|
|
export function useUpdateMapping() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ id, ...body }: { id: number; pattern: string; matchType: string; category: string; friendlyName?: string }) =>
|
|
request<AppMapping>({ method: 'PUT', url: `/mappings/${id}`, data: body }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['mappings'] }),
|
|
})
|
|
}
|
|
|
|
export function useDeleteMapping() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (id: number) => request<void>({ method: 'DELETE', url: `/mappings/${id}` }),
|
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['mappings'] }),
|
|
})
|
|
}
|
|
```
|
|
|
|
**Step 6: Add QueryClientProvider to main.tsx**
|
|
|
|
```tsx
|
|
import { StrictMode } from 'react'
|
|
import { createRoot } from 'react-dom/client'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import './index.css'
|
|
import App from './App'
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { staleTime: 10_000, retry: 1 } },
|
|
})
|
|
|
|
createRoot(document.getElementById('root')!).render(
|
|
<StrictMode>
|
|
<QueryClientProvider client={queryClient}>
|
|
<App />
|
|
</QueryClientProvider>
|
|
</StrictMode>,
|
|
)
|
|
```
|
|
|
|
**Step 7: Verify compilation**
|
|
|
|
```bash
|
|
cd TaskTracker.Web && npm run build
|
|
```
|
|
|
|
Expected: clean build with no errors.
|
|
|
|
**Step 8: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/types/ TaskTracker.Web/src/api/ TaskTracker.Web/src/main.tsx
|
|
git commit -m "feat: add TypeScript types, API client, and TanStack Query hooks"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Layout shell — sidebar, top bar, routing
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/components/Layout.tsx`
|
|
- Create: `TaskTracker.Web/src/pages/Board.tsx` (placeholder)
|
|
- Create: `TaskTracker.Web/src/pages/Analytics.tsx` (placeholder)
|
|
- Create: `TaskTracker.Web/src/pages/Mappings.tsx` (placeholder)
|
|
- Modify: `TaskTracker.Web/src/App.tsx`
|
|
|
|
**Step 1: Create Layout component**
|
|
|
|
Create `TaskTracker.Web/src/components/Layout.tsx` with:
|
|
- Collapsible sidebar (60px collapsed, 200px expanded) with gradient background
|
|
- Nav items: Board (LayoutGrid icon), Analytics (BarChart3 icon), Mappings (Link icon)
|
|
- Top bar with app title and SearchBar placeholder
|
|
- `<Outlet />` for page content
|
|
- Use `lucide-react` for icons
|
|
- Use `react-router-dom` `NavLink` with active styling (indigo highlight)
|
|
|
|
**Step 2: Create placeholder pages**
|
|
|
|
Each page as a simple component with just a heading, e.g.:
|
|
```tsx
|
|
export default function Board() {
|
|
return <div><h1 className="text-xl font-semibold text-white">Board</h1></div>
|
|
}
|
|
```
|
|
|
|
**Step 3: Wire up routing in App.tsx**
|
|
|
|
```tsx
|
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
|
import Layout from './components/Layout'
|
|
import Board from './pages/Board'
|
|
import Analytics from './pages/Analytics'
|
|
import Mappings from './pages/Mappings'
|
|
|
|
export default function App() {
|
|
return (
|
|
<BrowserRouter>
|
|
<Routes>
|
|
<Route element={<Layout />}>
|
|
<Route path="/" element={<Navigate to="/board" replace />} />
|
|
<Route path="/board" element={<Board />} />
|
|
<Route path="/analytics" element={<Analytics />} />
|
|
<Route path="/mappings" element={<Mappings />} />
|
|
</Route>
|
|
</Routes>
|
|
</BrowserRouter>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Step 4: Verify navigation works**
|
|
|
|
Run `npm run dev`. Click sidebar links — URL changes and placeholder pages render.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/
|
|
git commit -m "feat: add layout shell with sidebar navigation and routing"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Kanban board — columns, cards, drag-and-drop
|
|
|
|
This is the core feature. Build it incrementally.
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/lib/constants.ts` (colors, status labels)
|
|
- Create: `TaskTracker.Web/src/components/KanbanColumn.tsx`
|
|
- Create: `TaskTracker.Web/src/components/TaskCard.tsx`
|
|
- Create: `TaskTracker.Web/src/components/KanbanBoard.tsx`
|
|
- Modify: `TaskTracker.Web/src/pages/Board.tsx`
|
|
|
|
**Step 1: Create constants for status/category config**
|
|
|
|
Create `TaskTracker.Web/src/lib/constants.ts`:
|
|
```typescript
|
|
import { WorkTaskStatus } from '../types'
|
|
|
|
export const COLUMN_CONFIG = [
|
|
{ status: WorkTaskStatus.Pending, label: 'Pending', color: '#94a3b8' },
|
|
{ status: WorkTaskStatus.Active, label: 'Active', color: '#06b6d4' },
|
|
{ status: WorkTaskStatus.Paused, label: 'Paused', color: '#f59e0b' },
|
|
{ status: WorkTaskStatus.Completed, label: 'Completed', color: '#10b981' },
|
|
] as const
|
|
|
|
export const CATEGORY_COLORS: Record<string, string> = {
|
|
Development: '#6366f1',
|
|
Research: '#06b6d4',
|
|
Communication: '#8b5cf6',
|
|
DevOps: '#f97316',
|
|
Documentation: '#14b8a6',
|
|
Design: '#ec4899',
|
|
Unknown: '#64748b',
|
|
}
|
|
```
|
|
|
|
**Step 2: Build TaskCard component**
|
|
|
|
Create `TaskTracker.Web/src/components/TaskCard.tsx`:
|
|
- Renders task title, category badge (color-coded from `CATEGORY_COLORS`), elapsed time
|
|
- If `estimatedMinutes` set, show progress bar (elapsed / estimated)
|
|
- If task has subtasks, show "N/M done" indicator
|
|
- Active task gets a pulsing cyan border (`animate-pulse` or custom keyframe)
|
|
- Colored left border matching category
|
|
- Uses `useSortable` from dnd-kit for drag handle
|
|
- `onClick` prop to open detail panel
|
|
|
|
**Step 3: Build KanbanColumn component**
|
|
|
|
Create `TaskTracker.Web/src/components/KanbanColumn.tsx`:
|
|
- Column header with status label + count + colored underline
|
|
- Uses `useDroppable` from dnd-kit
|
|
- Renders list of `TaskCard` components
|
|
- "Pending" column has "+ Add Task" button at bottom
|
|
- Visual drop indicator when dragging over
|
|
|
|
**Step 4: Build KanbanBoard component**
|
|
|
|
Create `TaskTracker.Web/src/components/KanbanBoard.tsx`:
|
|
- `DndContext` + `SortableContext` from dnd-kit wrapping the columns
|
|
- `useTasks()` hook to fetch all tasks
|
|
- Groups tasks by status into columns
|
|
- `onDragEnd` handler that:
|
|
- Determines source/target column
|
|
- Maps column transitions to API calls (start/pause/resume/complete)
|
|
- Blocks invalid transitions (e.g., moving to Pending)
|
|
- Shows error toast on failure
|
|
- Filters out subtasks from board (only show top-level tasks, `parentTaskId === null`)
|
|
|
|
**Step 5: Wire Board page**
|
|
|
|
Update `TaskTracker.Web/src/pages/Board.tsx` to render `<KanbanBoard />` and pass an `onTaskClick` callback that opens the detail panel (wired in Task 6).
|
|
|
|
**Step 6: Verify board renders with real data**
|
|
|
|
Start the API (`dotnet run --project TaskTracker.Api`), then `npm run dev`. Create a few tasks via Swagger, verify they show in the correct columns. Test drag-and-drop transitions.
|
|
|
|
**Step 7: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/
|
|
git commit -m "feat: implement Kanban board with drag-and-drop task management"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Task detail slide-over panel
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/components/TaskDetailPanel.tsx`
|
|
- Create: `TaskTracker.Web/src/components/SubtaskList.tsx`
|
|
- Create: `TaskTracker.Web/src/components/NotesList.tsx`
|
|
- Modify: `TaskTracker.Web/src/pages/Board.tsx`
|
|
|
|
**Step 1: Build TaskDetailPanel**
|
|
|
|
Create `TaskTracker.Web/src/components/TaskDetailPanel.tsx`:
|
|
- Slide-over panel from right (~400px), board dimmed behind with semi-transparent overlay
|
|
- Fetches task detail via `useTask(id)`
|
|
- Sections:
|
|
- **Header**: Title (click to edit inline, blur to save via `useUpdateTask`), status badge, category dropdown
|
|
- **Description**: Click-to-edit text area, saves on blur
|
|
- **Time**: Show elapsed time. Input for estimated minutes, saves on blur. Progress bar if estimate set.
|
|
- **Subtasks**: `<SubtaskList />` component
|
|
- **Notes**: `<NotesList />` component
|
|
- **Actions**: Context-aware buttons — Start/Pause/Resume/Complete/Abandon based on current status
|
|
- Close on Escape keypress or overlay click
|
|
- Animate slide-in/out with CSS transition
|
|
|
|
**Step 2: Build SubtaskList**
|
|
|
|
Create `TaskTracker.Web/src/components/SubtaskList.tsx`:
|
|
- Renders `task.subTasks` as checkboxes
|
|
- Checking a subtask calls `useCompleteTask()` on it
|
|
- "+" button at top shows inline text input → `useCreateTask()` with `parentTaskId`
|
|
- Shows subtask status (completed = strikethrough + checkmark)
|
|
|
|
**Step 3: Build NotesList**
|
|
|
|
Create `TaskTracker.Web/src/components/NotesList.tsx`:
|
|
- Renders `task.notes` chronologically
|
|
- Each note shows: type badge (PauseNote/ResumeNote/General), content, relative timestamp
|
|
- "+" button shows inline text input → `POST /api/tasks/{id}/notes` with type General
|
|
|
|
**Step 4: Wire panel into Board page**
|
|
|
|
In `Board.tsx`:
|
|
- Add `selectedTaskId` state
|
|
- Pass `onTaskClick={(id) => setSelectedTaskId(id)}` to `KanbanBoard`
|
|
- Render `<TaskDetailPanel taskId={selectedTaskId} onClose={() => setSelectedTaskId(null)} />` when set
|
|
|
|
**Step 5: Verify full workflow**
|
|
|
|
Test: click card → panel opens → edit title → add subtask → add note → change status → close panel. All changes persist.
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/
|
|
git commit -m "feat: add task detail slide-over panel with inline editing, subtasks, and notes"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: Create task form
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/components/CreateTaskForm.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/KanbanColumn.tsx`
|
|
|
|
**Step 1: Build CreateTaskForm**
|
|
|
|
Create `TaskTracker.Web/src/components/CreateTaskForm.tsx`:
|
|
- Inline form that expands in the Pending column when "+ Add Task" is clicked
|
|
- Fields: Title (required), Description (optional textarea), Category (dropdown from known categories), Estimated Minutes (optional number)
|
|
- Submit calls `useCreateTask()`
|
|
- Escape or click away cancels
|
|
- Auto-focus title input on open
|
|
|
|
**Step 2: Wire into Pending column**
|
|
|
|
Modify `KanbanColumn.tsx`:
|
|
- When column is Pending, show "+ Add Task" button
|
|
- On click, toggle showing `<CreateTaskForm />` at bottom of column
|
|
- On submit/cancel, hide the form
|
|
|
|
**Step 3: Verify**
|
|
|
|
Create a task via the inline form. It should appear in the Pending column immediately.
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/
|
|
git commit -m "feat: add inline create task form in Pending column"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 8: Search bar + board filters
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/components/SearchBar.tsx`
|
|
- Create: `TaskTracker.Web/src/components/FilterBar.tsx`
|
|
- Modify: `TaskTracker.Web/src/components/Layout.tsx`
|
|
- Modify: `TaskTracker.Web/src/pages/Board.tsx`
|
|
|
|
**Step 1: Build SearchBar**
|
|
|
|
Create `TaskTracker.Web/src/components/SearchBar.tsx`:
|
|
- Input with search icon (lucide `Search`)
|
|
- On typing, filters tasks client-side (title + description match)
|
|
- Results shown as dropdown list of matching tasks
|
|
- Click result → opens TaskDetailPanel (via callback prop)
|
|
- Keyboard navigation: arrow keys to move, Enter to select, Escape to close
|
|
- Debounce input by 200ms
|
|
|
|
**Step 2: Build FilterBar**
|
|
|
|
Create `TaskTracker.Web/src/components/FilterBar.tsx`:
|
|
- Row of filter chips rendered below the board header
|
|
- Category chips: derived from unique categories across all tasks + mappings
|
|
- "Has subtasks" toggle chip
|
|
- Active filters shown as colored chips with "x" to dismiss
|
|
- Exposes `filters` state and `filteredTasks` computation to parent
|
|
|
|
**Step 3: Wire SearchBar into Layout**
|
|
|
|
Add `<SearchBar />` to the top bar in `Layout.tsx`. It needs access to task data — use `useTasks()` inside the component.
|
|
|
|
**Step 4: Wire FilterBar into Board page**
|
|
|
|
In `Board.tsx`, add `<FilterBar />` above the `<KanbanBoard />`. Pass filtered tasks down to the board instead of all tasks. Lift task fetching up to `Board.tsx` and pass tasks as props to both `FilterBar` and `KanbanBoard`.
|
|
|
|
**Step 5: Verify**
|
|
|
|
- Type in search bar → dropdown shows matching tasks
|
|
- Click category chip → board filters to that category
|
|
- Combine filters → board shows intersection
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/
|
|
git commit -m "feat: add global search bar and board filter chips"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 9: Analytics page — timeline, charts, activity feed
|
|
|
|
**Files:**
|
|
- Create: `TaskTracker.Web/src/components/analytics/Timeline.tsx`
|
|
- Create: `TaskTracker.Web/src/components/analytics/CategoryBreakdown.tsx`
|
|
- Create: `TaskTracker.Web/src/components/analytics/ActivityFeed.tsx`
|
|
- Modify: `TaskTracker.Web/src/pages/Analytics.tsx`
|
|
|
|
**Step 1: Build Analytics page shell**
|
|
|
|
Update `TaskTracker.Web/src/pages/Analytics.tsx`:
|
|
- Filter bar at top: time range dropdown (Today/7d/30d), task filter dropdown
|
|
- Time range maps to `minutes` param for `/context/recent` (1440 for today, 10080 for 7d, 43200 for 30d)
|
|
- Three sections stacked vertically: Timeline, Category Breakdown, Activity Feed
|
|
|
|
**Step 2: Build Timeline component**
|
|
|
|
Create `TaskTracker.Web/src/components/analytics/Timeline.tsx`:
|
|
- Recharts `BarChart` with custom rendering
|
|
- X-axis: time of day (hourly buckets)
|
|
- Bars: colored by category (from mappings)
|
|
- Tooltip on hover: app name, window title, duration
|
|
- Uses `useRecentContext()` data, groups events into time buckets
|
|
|
|
**Step 3: Build CategoryBreakdown component**
|
|
|
|
Create `TaskTracker.Web/src/components/analytics/CategoryBreakdown.tsx`:
|
|
- Left side: Recharts `PieChart` (donut style with inner radius)
|
|
- Right side: list of categories with horizontal bar, time, percentage
|
|
- Uses `useContextSummary()` data
|
|
- Colors from `CATEGORY_COLORS` constant
|
|
|
|
**Step 4: Build ActivityFeed component**
|
|
|
|
Create `TaskTracker.Web/src/components/analytics/ActivityFeed.tsx`:
|
|
- Reverse-chronological list of context events
|
|
- Each row: colored category dot, relative timestamp, app name (bold), window title/URL
|
|
- "Load more" button at bottom (increase `minutes` param or paginate client-side)
|
|
- Uses `useRecentContext()` with the selected time range
|
|
|
|
**Step 5: Verify analytics page**
|
|
|
|
Need some context data — either run WindowWatcher/Chrome extension to generate real data, or POST a few test events via Swagger. Verify all three visualizations render.
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/
|
|
git commit -m "feat: add analytics page with timeline, category breakdown, and activity feed"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 10: Mappings page
|
|
|
|
**Files:**
|
|
- Modify: `TaskTracker.Web/src/pages/Mappings.tsx`
|
|
|
|
**Step 1: Build Mappings page**
|
|
|
|
Update `TaskTracker.Web/src/pages/Mappings.tsx`:
|
|
- Table with columns: Pattern, Match Type, Category (color badge), Friendly Name, Actions (edit/delete)
|
|
- Uses `useMappings()` to fetch data
|
|
- "+ Add Rule" button at top → inserts inline form row at top of table
|
|
- Form fields: Pattern (text), Match Type (dropdown: ProcessName/TitleContains/UrlContains), Category (text), Friendly Name (text)
|
|
- Submit → `useCreateMapping()`
|
|
- Edit button → row becomes editable inline → save → `useUpdateMapping()`
|
|
- Delete button → confirm dialog → `useDeleteMapping()`
|
|
|
|
**Step 2: Verify CRUD**
|
|
|
|
Create a mapping, edit it, delete it. Verify all operations work.
|
|
|
|
**Step 3: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/src/
|
|
git commit -m "feat: add mappings page with inline CRUD table"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 11: Visual polish and animations
|
|
|
|
**Files:**
|
|
- Modify: Various component files for animation/transition polish
|
|
- Modify: `TaskTracker.Web/index.html` (add Inter font)
|
|
|
|
**Step 1: Add Inter font**
|
|
|
|
In `TaskTracker.Web/index.html`, add to `<head>`:
|
|
```html
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
```
|
|
|
|
Add to `index.css` (inside `@theme`):
|
|
```css
|
|
@theme {
|
|
--font-sans: 'Inter', sans-serif;
|
|
}
|
|
```
|
|
|
|
**Step 2: Add animations**
|
|
|
|
- Active task card: glowing cyan border pulse (CSS `@keyframes` animation)
|
|
- Detail panel: slide-in from right with opacity transition
|
|
- Drag-and-drop: smooth card movement via dnd-kit's built-in transitions
|
|
- Hover effects: subtle card lift with colored shadow
|
|
- Column drop target: border highlight on drag-over
|
|
|
|
**Step 3: Polish visual details**
|
|
|
|
- Sidebar gradient background
|
|
- Colored left border on task cards
|
|
- Category badge colors consistent everywhere
|
|
- Column header colored underlines
|
|
- Colored-tint shadows on cards (not plain gray)
|
|
- Consistent spacing and typography
|
|
|
|
**Step 4: Verify visual quality**
|
|
|
|
Walk through every page and interaction. Check dark background rendering, color contrast, animation smoothness.
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add TaskTracker.Web/
|
|
git commit -m "feat: add visual polish — Inter font, animations, colored shadows, hover effects"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 12: Final integration testing and cleanup
|
|
|
|
**Step 1: Full workflow test**
|
|
|
|
Run both the API and the React dev server. Walk through the complete workflow:
|
|
1. Create a task from the board
|
|
2. Start it (drag or button)
|
|
3. Add a subtask, complete it
|
|
4. Add a note
|
|
5. Set time estimate, verify progress bar
|
|
6. Pause with note, resume
|
|
7. Complete the task
|
|
8. Check analytics page
|
|
9. Add/edit/delete a mapping
|
|
10. Test search and filters
|
|
11. Test sidebar collapse/expand
|
|
|
|
**Step 2: Fix any issues found**
|
|
|
|
Address bugs or visual issues found during testing.
|
|
|
|
**Step 3: Clean up**
|
|
|
|
- Remove any unused imports or dead code
|
|
- Ensure no console warnings in browser
|
|
- Verify `npm run build` produces a clean production build
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "chore: final cleanup and integration testing"
|
|
```
|