From 1d8f548f4ba75b1b121f4955bb2db5f5262638a1 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:25:50 +0000 Subject: [PATCH 1/9] 19: mobile responsive - reduce main content padding on mobile (p-4 md:p-6) --- apps/web/app/(dashboard)/layout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx index 044fa2c..42c483d 100644 --- a/apps/web/app/(dashboard)/layout.tsx +++ b/apps/web/app/(dashboard)/layout.tsx @@ -4,6 +4,7 @@ import { NetworkErrorBanner } from '@/components/network-error-banner'; import { KeyboardShortcutsProvider } from '@/components/keyboard-shortcuts-provider'; import { WebVitalsTracker } from '@/components/web-vitals-tracker'; import { MobileBottomNav } from '@/components/mobile-bottom-nav'; +import { OnboardingFlow } from '@/components/onboarding/onboarding-flow'; export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( @@ -17,12 +18,13 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
-
+
{children}
+ {}} />
); From f1b38ac4970270492e8f7d8b1b55e2bb480994ba Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:06 +0000 Subject: [PATCH 2/9] 21: habit analytics - recharts line/bar chart for 30-day completions and top streaks --- apps/web/app/(dashboard)/habits/page.tsx | 6 + .../web/components/habits/habit-analytics.tsx | 142 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 apps/web/components/habits/habit-analytics.tsx diff --git a/apps/web/app/(dashboard)/habits/page.tsx b/apps/web/app/(dashboard)/habits/page.tsx index 198ee8b..3b4514b 100644 --- a/apps/web/app/(dashboard)/habits/page.tsx +++ b/apps/web/app/(dashboard)/habits/page.tsx @@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge"; import { HabitCreateDialog } from "@/components/habits/habit-create-dialog"; import { HabitCompletionDialog } from "@/components/habits/habit-completion-dialog"; import { HabitCalendarHeatmap } from "@/components/habits/habit-calendar-heatmap"; +import { HabitAnalytics } from "@/components/habits/habit-analytics"; import { toast } from "sonner"; interface Habit { @@ -214,6 +215,11 @@ export default function HabitsPage() {
)} + {/* Analytics */} +
+ +
+ ([]); + const [completions, setCompletions] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!open) return; + setLoading(true); + + Promise.all([ + fetch('/api/habits/streaks').then((r) => r.json()), + ...habits.map((h) => + fetch( + '/api/domains/' + domainId + '/habits/' + h.id + '/completions?from=' + daysAgo(30) + '&order=asc&limit=365' + ).then((r) => r.json()) + ), + ]) + .then(([streaksData, ...completionsData]) => { + setStreaks((streaksData.streaks || []).slice(0, 5)); + + const dateMap = new Map(); + for (const data of completionsData) { + for (const item of data.items || []) { + const d = item.date?.split('T')[0]; + if (d) dateMap.set(d, (dateMap.get(d) || 0) + 1); + } + } + const sorted = Array.from(dateMap.entries()) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([date, count]) => ({ date, count })); + setCompletions(sorted); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, [open, domainId, habits]); + + if (!open) { + return ( + + ); + } + + return ( +
+
+

Analytics (30 days)

+ +
+ + {loading ? ( +

Loading analytics...

+ ) : ( +
+ + + + + Daily Completions + + + + {completions.length === 0 ? ( +

No data yet

+ ) : ( + + + + v.slice(5)} /> + + + + + + )} +
+
+ + + + + + Top Streaks + + + + {streaks.length === 0 ? ( +

No streaks yet

+ ) : ( + + + + + + + + + + )} +
+
+
+ )} +
+ ); +} + +function daysAgo(n: number): string { + const d = new Date(); + d.setDate(d.getDate() - n); + return d.toISOString().split('T')[0]; +} From dd431a35d60836e9bfa3f868676327b5b942d0fe Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:10 +0000 Subject: [PATCH 3/9] 22: project timeline / Gantt view - CSS grid horizontal Gantt with sections as rows --- .../app/(dashboard)/projects/[id]/page.tsx | 4 + .../components/projects/project-timeline.tsx | 126 ++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 apps/web/components/projects/project-timeline.tsx diff --git a/apps/web/app/(dashboard)/projects/[id]/page.tsx b/apps/web/app/(dashboard)/projects/[id]/page.tsx index 73e7f95..b984c22 100644 --- a/apps/web/app/(dashboard)/projects/[id]/page.tsx +++ b/apps/web/app/(dashboard)/projects/[id]/page.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Progress } from "@/components/ui/progress"; import { SectionDialog } from "@/components/projects/section-dialog"; +import { ProjectTimeline } from "@/components/projects/project-timeline"; import Link from "next/link"; import { toast } from "sonner"; @@ -290,6 +291,9 @@ export default function ProjectDetailPage() { + {/* Timeline */} + + = { + planned: 'bg-gray-200 dark:bg-gray-700', + in_progress: 'bg-blue-400 dark:bg-blue-600', + complete: 'bg-green-400 dark:bg-green-600', +}; + +const kindBadge: Record = { + section: 'bg-muted text-muted-foreground', + milestone: 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200', +}; + +export function ProjectTimeline({ sections, projectTargetDate }: ProjectTimelineProps) { + const { startDate, totalDays } = useMemo(() => { + if (sections.length === 0) return { startDate: new Date(), totalDays: 30 }; + + const dates = sections + .filter((s) => s.targetDate) + .map((s) => new Date(s.targetDate!)); + + if (projectTargetDate) dates.push(new Date(projectTargetDate)); + + if (dates.length === 0) { + // No dates at all — show a default 30-day window + const now = new Date(); + return { startDate: now, totalDays: 30 }; + } + + const minDate = new Date(Math.min(...dates.map((d) => d.getTime()))); + const maxDate = new Date(Math.max(...dates.map((d) => d.getTime()))); + const diff = Math.max((maxDate.getTime() - minDate.getTime()) / (1000 * 60 * 60 * 24), 14); + return { startDate: minDate, totalDays: Math.ceil(diff) }; + }, [sections, projectTargetDate]); + + if (sections.length === 0) return null; + + return ( +
+

Timeline

+
+ {/* Header row */} +
+ Section +
+
+ {Array.from({ length: Math.min(totalDays, 60) }).map((_, i) => ( +
+ ))} +
+
+
+ + {/* Section rows */} +
+ {sections.map((section) => { + if (!section.targetDate) return null; + + const sectionDate = new Date(section.targetDate); + const dayOffset = Math.max( + 0, + Math.round((sectionDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + ); + const barWidth = Math.max(8, Math.min(100, (1 / Math.max(totalDays, 1)) * 100)); + + return ( +
+
+ + {section.kind === 'milestone' ? 'M' : 'S'} + + {section.name} +
+
+
+
+
+ ); + })} +
+ + {/* Project target date marker */} + {projectTargetDate && ( +
+ Target date + {new Date(projectTargetDate).toLocaleDateString()} +
+ )} +
+
+ ); +} From 0835961bc9ab660c4c2ab50b839e38f7bcc2caf8 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:13 +0000 Subject: [PATCH 4/9] 23: note templates - localStorage templates (Meeting notes, Daily journal, Brain dump) --- apps/web/app/(dashboard)/notes/page.tsx | 26 ++++ apps/web/components/notes/note-templates.tsx | 124 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 apps/web/components/notes/note-templates.tsx diff --git a/apps/web/app/(dashboard)/notes/page.tsx b/apps/web/app/(dashboard)/notes/page.tsx index fa07576..a05f763 100644 --- a/apps/web/app/(dashboard)/notes/page.tsx +++ b/apps/web/app/(dashboard)/notes/page.tsx @@ -5,6 +5,7 @@ import { Plus, FileText, Link2, GitBranch, Trash2, Pin, Archive, Search, PinOff, import dynamic from 'next/dynamic'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; +import { NoteTemplates } from '@/components/notes/note-templates'; import { Card } from '@/components/ui/card'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Badge } from '@/components/ui/badge'; @@ -162,6 +163,28 @@ export default function NotesPage() { } } + async function createNoteWithContent(content: string) { + if (!domainId) return; + try { + const response = await fetch("/api/domains/" + domainId + "/notes", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: 'Untitled note', + content, + }), + }); + if (!response.ok) throw new Error('Unable to create note.'); + const newNote = await response.json(); + setNotes((current) => [newNote, ...current]); + setSelectedNote(newNote); + toast.success('Note created from template'); + } catch (error) { + console.error('Failed to create note:', error); + toast.error('Unable to create note'); + } + } + async function createNote() { if (!domainId) return; try { @@ -315,6 +338,9 @@ export default function NotesPage() { ))} )} + { + createNoteWithContent(content); + }} /> + + + + + Choose a template + + Start with a pre-formatted note template. + + + +
+ {templates.map((template) => ( + + ))} +
+ + + Templates are stored locally in your browser. + +
+
+ + ); +} From cd5d7a96ffd01c4cd907c3ccc1fa65496d69237d Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:17 +0000 Subject: [PATCH 5/9] 28: task templates - localStorage templates (Bug fix, Feature work, Quick meeting) in create dialog --- .../components/tasks/task-create-dialog.tsx | 8 ++ apps/web/components/tasks/task-templates.tsx | 96 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 apps/web/components/tasks/task-templates.tsx diff --git a/apps/web/components/tasks/task-create-dialog.tsx b/apps/web/components/tasks/task-create-dialog.tsx index 434e5c9..93ad31f 100644 --- a/apps/web/components/tasks/task-create-dialog.tsx +++ b/apps/web/components/tasks/task-create-dialog.tsx @@ -21,6 +21,7 @@ import { SelectValue, } from '@/components/ui/select'; import { toast } from 'sonner'; +import { TaskTemplates } from '@/components/tasks/task-templates'; interface TaskCreateDialogProps { open: boolean; @@ -107,6 +108,13 @@ export function TaskCreateDialog({ Create a new task to track your work.
+ { + setTitle(t.title); + setDescription(t.description); + setPriority(t.priority); + setStatus(t.status); + }} /> +
void; +} + +export function TaskTemplates({ onSelect }: TaskTemplatesProps) { + const [templates] = useState(getTemplates); + + const handleChange = useCallback( + (value: string) => { + const template = templates.find((t) => t.name === value); + if (template) { + onSelect(template); + toast.success('Template applied'); + } + }, + [templates, onSelect] + ); + + if (templates.length === 0) return null; + + return ( +
+ + +
+ ); +} + +export type { TaskTemplate }; From 39e182418d2978383cb510d6627b71a726e59d33 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:23 +0000 Subject: [PATCH 6/9] 27: onboarding flow - first-run sheet with 3 steps (domain, task, habit) --- .../components/onboarding/onboarding-flow.tsx | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 apps/web/components/onboarding/onboarding-flow.tsx diff --git a/apps/web/components/onboarding/onboarding-flow.tsx b/apps/web/components/onboarding/onboarding-flow.tsx new file mode 100644 index 0000000..0c60f72 --- /dev/null +++ b/apps/web/components/onboarding/onboarding-flow.tsx @@ -0,0 +1,151 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { toast } from 'sonner'; +import { Rocket, ListTodo, Flame } from 'lucide-react'; + +const ONBOARDED_KEY = 'pe_onboarded'; + +interface OnboardingFlowProps { + onComplete: () => void; +} + +export function OnboardingFlow({ onComplete }: OnboardingFlowProps) { + const [open, setOpen] = useState(false); + const [step, setStep] = useState(0); + const [domainName, setDomainName] = useState(''); + + useEffect(() => { + if (typeof window === 'undefined') return; + const onboarded = localStorage.getItem(ONBOARDED_KEY); + if (!onboarded) { + setOpen(true); + } + }, []); + + function handleDismiss() { + localStorage.setItem(ONBOARDED_KEY, 'true'); + setOpen(false); + onComplete(); + } + + async function handleCreateDomain() { + if (!domainName.trim()) { + toast.error('Please enter a domain name'); + return; + } + try { + const res = await fetch('/api/domains', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: domainName.trim() }), + }); + if (!res.ok) throw new Error('Failed to create domain'); + toast.success('Domain created!'); + setStep(1); + } catch { + toast.error('Failed to create domain'); + } + } + + const steps = [ + { + title: 'Pick your primary domain', + description: 'A domain is your workspace — a container for tasks, habits, and projects.', + icon: , + content: ( +
+ + setDomainName(e.target.value)} + placeholder="e.g. Personal, Work, Side Project" + autoFocus + /> + +
+ ), + }, + { + title: 'Create your first task', + description: 'Tasks are the building blocks of your workflow. Create one to get started.', + icon: , + content: ( +
+

+ Use the + button in the top bar or press{' '} + C on the Tasks page to create a new task. +

+ +
+ ), + }, + { + title: 'Add a habit', + description: 'Build streaks and track progress on things you do regularly.', + icon: , + content: ( +
+

+ Head to the Habits page and click New habit to start tracking something daily or weekly. +

+ +
+ ), + }, + ]; + + const current = steps[step]; + + return ( + { if (!v) handleDismiss(); }}> + + +
+ {current.icon} +
+ {current.title} + {current.description} +
+
+
+ + {current.content} + + +
+ {steps.map((_, i) => ( +
+ ))} +
+ + + + + ); +} From e0020eac7d781cffce93686babc1a7c09eae9168 Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 19:26:27 +0000 Subject: [PATCH 7/9] 26: domain-specific dashboard - respect ?domain= query param --- apps/web/app/(dashboard)/dashboard/page.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(dashboard)/dashboard/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx index 5d2a557..b0b28db 100644 --- a/apps/web/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/page.tsx @@ -6,7 +6,7 @@ import { useDashboardStore } from '@/lib/stores/use-dashboard-store'; import { WidgetErrorBoundary } from '@/components/widget-error-boundary'; import { Button } from '@/components/ui/button'; import { Settings2, LayoutGrid } from 'lucide-react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; // Lazy load react-grid-layout (client-only, ~45KB) const ResponsiveGridLayout = dynamic( @@ -74,6 +74,8 @@ export default function DashboardPage() { const [editMode, setEditMode] = React.useState(false); const [showConfig, setShowConfig] = React.useState(false); const router = useRouter(); + const searchParams = useSearchParams(); + const domainFilter = searchParams.get('domain'); const layout = widgets.map((w) => ({ i: w.id, @@ -113,7 +115,7 @@ export default function DashboardPage() {

Dashboard

-

Your day, at a glance.

+

Your day, at a glance.{domainFilter ? " (Filtered: " + domainFilter + ")" : ""}

); } +export default function DashboardPageWrapper() { + return ( + Loading dashboard...
}> + + + ); +} From efbab9517c044eff886587085d643292c13c50dc Mon Sep 17 00:00:00 2001 From: bot-hermes Date: Wed, 29 Jul 2026 20:23:12 +0000 Subject: [PATCH 9/9] 27: onboarding flow - make onComplete optional for server component layout --- apps/web/app/(dashboard)/layout.tsx | 2 +- apps/web/components/onboarding/onboarding-flow.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx index 42c483d..a9760ef 100644 --- a/apps/web/app/(dashboard)/layout.tsx +++ b/apps/web/app/(dashboard)/layout.tsx @@ -24,7 +24,7 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
- {}} /> +
); diff --git a/apps/web/components/onboarding/onboarding-flow.tsx b/apps/web/components/onboarding/onboarding-flow.tsx index 0c60f72..43de0fd 100644 --- a/apps/web/components/onboarding/onboarding-flow.tsx +++ b/apps/web/components/onboarding/onboarding-flow.tsx @@ -18,7 +18,7 @@ import { Rocket, ListTodo, Flame } from 'lucide-react'; const ONBOARDED_KEY = 'pe_onboarded'; interface OnboardingFlowProps { - onComplete: () => void; + onComplete?: () => void; } export function OnboardingFlow({ onComplete }: OnboardingFlowProps) { @@ -37,7 +37,7 @@ export function OnboardingFlow({ onComplete }: OnboardingFlowProps) { function handleDismiss() { localStorage.setItem(ONBOARDED_KEY, 'true'); setOpen(false); - onComplete(); + onComplete?.(); } async function handleCreateDomain() {