From edd5e1227428d5fc64692f1415c3d4b8b846887a Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Mon, 23 Feb 2026 14:21:44 +0500 Subject: [PATCH 01/78] fix: add loading state and disabled guard to Google OAuth buttons on signup and login pages --- website/app/login/page.tsx | 30 ++++++++++++++++++++++-------- website/app/signup/page.tsx | 30 ++++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/website/app/login/page.tsx b/website/app/login/page.tsx index 376c5150..29f7f055 100644 --- a/website/app/login/page.tsx +++ b/website/app/login/page.tsx @@ -12,6 +12,7 @@ export default function LoginPage() { const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + const [oauthLoading, setOauthLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -40,6 +41,8 @@ export default function LoginPage() { const handleGoogleSignIn = async () => { if (!isLoaded) return; + setOauthLoading(true); + setError(""); try { await signIn.authenticateWithRedirect({ strategy: "oauth_google", @@ -49,6 +52,7 @@ export default function LoginPage() { } catch (err: unknown) { const clerkError = err as { errors?: { message: string }[] }; setError(clerkError.errors?.[0]?.message || "OAuth sign in failed"); + setOauthLoading(false); } }; @@ -84,15 +88,25 @@ export default function LoginPage() {
diff --git a/website/app/signup/page.tsx b/website/app/signup/page.tsx index 59bf7f90..c08e945d 100644 --- a/website/app/signup/page.tsx +++ b/website/app/signup/page.tsx @@ -14,6 +14,7 @@ export default function SignupPage() { const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); + const [oauthLoading, setOauthLoading] = useState(false); const [pendingVerification, setPendingVerification] = useState(false); const [code, setCode] = useState(""); @@ -66,6 +67,8 @@ export default function SignupPage() { const handleGoogleSignUp = async () => { if (!isLoaded) return; + setOauthLoading(true); + setError(""); try { await signUp.authenticateWithRedirect({ strategy: "oauth_google", @@ -75,6 +78,7 @@ export default function SignupPage() { } catch (err: unknown) { const clerkError = err as { errors?: { message: string }[] }; setError(clerkError.errors?.[0]?.message || "OAuth sign up failed"); + setOauthLoading(false); } }; @@ -154,15 +158,25 @@ export default function SignupPage() {
From d67d616739e4780f254327b624f1aab128254318 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Mon, 23 Feb 2026 21:44:08 +0500 Subject: [PATCH 02/78] feat(dashboard): build habit dashboard UI with donut chart, heatmap, goals, activity logs, and UI polish --- .../app/components/dashboard/ActivityItem.tsx | 81 ++++++ .../app/components/dashboard/ActivityList.tsx | 43 +++ .../components/dashboard/ActivityLogList.tsx | 56 ++++ .../dashboard/ActivityLogsSection.tsx | 100 +++++++ .../dashboard/ClassificationPanel.tsx | 160 +++++++++++ .../app/components/dashboard/DonutChart.tsx | 141 ++++++++++ website/app/components/dashboard/GoalCard.tsx | 153 +++++++++++ .../app/components/dashboard/GoalsSection.tsx | 180 +++++++++++++ .../app/components/dashboard/HabitHeatmap.tsx | 250 ++++++++++++++++++ .../components/dashboard/TodayOverview.tsx | 57 ++++ website/app/dashboard/page.tsx | 16 ++ website/app/globals.css | 20 ++ 12 files changed, 1257 insertions(+) create mode 100644 website/app/components/dashboard/ActivityItem.tsx create mode 100644 website/app/components/dashboard/ActivityList.tsx create mode 100644 website/app/components/dashboard/ActivityLogList.tsx create mode 100644 website/app/components/dashboard/ActivityLogsSection.tsx create mode 100644 website/app/components/dashboard/ClassificationPanel.tsx create mode 100644 website/app/components/dashboard/DonutChart.tsx create mode 100644 website/app/components/dashboard/GoalCard.tsx create mode 100644 website/app/components/dashboard/GoalsSection.tsx create mode 100644 website/app/components/dashboard/HabitHeatmap.tsx create mode 100644 website/app/components/dashboard/TodayOverview.tsx diff --git a/website/app/components/dashboard/ActivityItem.tsx b/website/app/components/dashboard/ActivityItem.tsx new file mode 100644 index 00000000..b4bd10bb --- /dev/null +++ b/website/app/components/dashboard/ActivityItem.tsx @@ -0,0 +1,81 @@ +// ─── Shared types & config (used by ActivityLogList + ClassificationPanel) ──── + +export type LogCategory = "Work" | "Health" | "Relationships"; + +export interface ActivityLog { + id: string; + time: string; // "HH:MM" + text: string; + category: LogCategory; +} + +export const CATEGORY_CONFIG: Record< + LogCategory, + { badge: string; color: string; highlight: string } +> = { + Work: { + badge: "bg-blue-500/15 text-blue-300", + color: "#3b82f6", + highlight: "bg-blue-500/20 text-blue-200 not-italic", + }, + Health: { + badge: "bg-emerald-500/15 text-emerald-300", + color: "#10b981", + highlight: "bg-emerald-500/20 text-emerald-200 not-italic", + }, + Relationships: { + badge: "bg-amber-500/15 text-amber-300", + color: "#f59e0b", + highlight: "bg-amber-500/20 text-amber-200 not-italic", + }, +}; + +// ─── ActivityItem ───────────────────────────────────────────────────────────── + +interface ActivityItemProps { + log: ActivityLog; + isLast: boolean; + isLatest: boolean; +} + +export default function ActivityItem({ log, isLast, isLatest }: ActivityItemProps) { + const config = CATEGORY_CONFIG[log.category]; + + return ( +
+ {/* Left col: time + connecting line */} +
+ + {log.time} + + {!isLast && ( +
+ )} +
+ + {/* Dot */} +
+
+ {!isLast &&
} +
+ + {/* Content */} +
+

{log.text}

+ + {log.category} + +
+
+ ); +} diff --git a/website/app/components/dashboard/ActivityList.tsx b/website/app/components/dashboard/ActivityList.tsx new file mode 100644 index 00000000..0052ae8e --- /dev/null +++ b/website/app/components/dashboard/ActivityList.tsx @@ -0,0 +1,43 @@ +export interface Activity { + id: string; + title: string; + category: "Work" | "Health" | "Relationships"; + time: string; + icon: string; +} + +interface ActivityListProps { + activities: Activity[]; +} + +const CATEGORY_STYLES: Record = { + Work: "bg-blue-500/15 text-blue-300", + Health: "bg-emerald-500/15 text-emerald-300", + Relationships: "bg-amber-500/15 text-amber-300", +}; + +export default function ActivityList({ activities }: ActivityListProps) { + return ( +
+ {activities.map((activity) => ( +
+ {activity.icon} +
+

{activity.title}

+

{activity.time}

+
+ + {activity.category} + +
+ ))} +
+ ); +} diff --git a/website/app/components/dashboard/ActivityLogList.tsx b/website/app/components/dashboard/ActivityLogList.tsx new file mode 100644 index 00000000..f38b472a --- /dev/null +++ b/website/app/components/dashboard/ActivityLogList.tsx @@ -0,0 +1,56 @@ +import ActivityItem, { ActivityLog } from "./ActivityItem"; + +// ─── Mock data ──────────────────────────────────────────────────────────────── + +export const MOCK_LOGS: ActivityLog[] = [ + { id: "1", time: "07:00", text: "Morning workout at the gym", category: "Health" }, + { id: "2", time: "08:15", text: "Journaled thoughts for 15 minutes", category: "Health" }, + { id: "3", time: "09:30", text: "Team standup meeting", category: "Work" }, + { id: "4", time: "10:00", text: "Deep work on project proposal", category: "Work" }, + { id: "5", time: "12:30", text: "Lunch break — took a walk outside", category: "Health" }, + { id: "6", time: "13:15", text: "Called mum to check in", category: "Relationships" }, + { id: "7", time: "14:00", text: "Project review with the team", category: "Work" }, + { id: "8", time: "17:30", text: "Dinner with family at home", category: "Relationships" }, + { id: "9", time: "19:00", text: "Evening run — 5 km", category: "Health" }, + { id: "10", time: "21:00", text: "Read before bed", category: "Health" }, +]; + +// ─── ActivityLogList ────────────────────────────────────────────────────────── + +interface ActivityLogListProps { + logs?: ActivityLog[]; +} + +export default function ActivityLogList({ logs = MOCK_LOGS }: ActivityLogListProps) { + const categoryCount = logs.reduce>((acc, l) => { + acc[l.category] = (acc[l.category] ?? 0) + 1; + return acc; + }, {}); + + return ( +
+
+

Today's Log

+
+ {Object.entries(categoryCount).map(([cat, count]) => ( + + {count} {cat} + + ))} +
+
+ + {/* Scrollable timeline */} +
+ {logs.map((log, i) => ( + + ))} +
+
+ ); +} diff --git a/website/app/components/dashboard/ActivityLogsSection.tsx b/website/app/components/dashboard/ActivityLogsSection.tsx new file mode 100644 index 00000000..3ebe2937 --- /dev/null +++ b/website/app/components/dashboard/ActivityLogsSection.tsx @@ -0,0 +1,100 @@ +import ClassificationPanel from "./ClassificationPanel"; +import ActivityLogList from "./ActivityLogList"; + +// ─── Pipeline step ──────────────────────────────────────────────────────────── + +function PipelineStep({ + icon, + label, + isLast = false, +}: { + icon: React.ReactNode; + label: string; + isLast?: boolean; +}) { + return ( + <> +
+ {icon} + {label} +
+ {!isLast && ( + + + + )} + + ); +} + +// ─── ActivityLogsSection ────────────────────────────────────────────────────── + +export default function ActivityLogsSection() { + return ( +
+ {/* Header */} +
+
+

Activity Logs

+

How your messages become insights

+
+ + Today + +
+ + {/* Pipeline indicator */} +
+ + + + } + label="WhatsApp message" + /> + + + + } + label="AI classifies intent" + /> + + + + } + label="Categorized log entry" + /> + + + + } + label="Dashboard insights" + isLast + /> +
+ + {/* Body: 2-column — Classification | Log timeline */} +
+ + +
+
+ ); +} diff --git a/website/app/components/dashboard/ClassificationPanel.tsx b/website/app/components/dashboard/ClassificationPanel.tsx new file mode 100644 index 00000000..ec66b64b --- /dev/null +++ b/website/app/components/dashboard/ClassificationPanel.tsx @@ -0,0 +1,160 @@ +import { CATEGORY_CONFIG, LogCategory } from "./ActivityItem"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface ClassificationSegment { + text: string; + category: LogCategory; +} + +interface ClassificationExample { + id: string; + rawInput: string; + segments: ClassificationSegment[]; +} + +// ─── Mock classification examples ───────────────────────────────────────────── + +const MOCK_CLASSIFICATIONS: ClassificationExample[] = [ + { + id: "1", + rawInput: "Had lunch with my parents and went for a run after", + segments: [ + { text: "lunch with my parents", category: "Relationships" }, + { text: "went for a run", category: "Health" }, + ], + }, + { + id: "2", + rawInput: "Deep work session all morning, then called Sarah", + segments: [ + { text: "Deep work session", category: "Work" }, + { text: "called Sarah", category: "Relationships" }, + ], + }, + { + id: "3", + rawInput: "Morning gym, team standup, reading before sleep", + segments: [ + { text: "Morning gym", category: "Health" }, + { text: "team standup", category: "Work" }, + { text: "reading before sleep", category: "Health" }, + ], + }, +]; + +// ─── Highlighted text renderer ──────────────────────────────────────────────── + +function HighlightedText({ + raw, + segments, +}: { + raw: string; + segments: ClassificationSegment[]; +}) { + const parts: { text: string; category: LogCategory | null }[] = []; + let remaining = raw; + + for (const seg of segments) { + const idx = remaining.indexOf(seg.text); + if (idx === -1) continue; + if (idx > 0) parts.push({ text: remaining.slice(0, idx), category: null }); + parts.push({ text: seg.text, category: seg.category }); + remaining = remaining.slice(idx + seg.text.length); + } + if (remaining.length > 0) parts.push({ text: remaining, category: null }); + + return ( +

+ “ + {parts.map((part, i) => + part.category ? ( + + {part.text} + + ) : ( + {part.text} + ) + )} + ” +

+ ); +} + +// ─── ClassificationPanel ────────────────────────────────────────────────────── + +export default function ClassificationPanel() { + return ( +
+

+ Data Classification +

+ +
+ {MOCK_CLASSIFICATIONS.map((ex) => ( +
+ {/* Input bubble */} +
+ {/* WhatsApp-style avatar dot */} +
+ + + +
+ +
+ + {/* Arrow + "classified as" label */} +
+ + + + + classified as + +
+ + {/* Extracted category tags */} +
+ {ex.segments.map((seg, i) => ( +
+ + {seg.category} +
+ ))} +
+
+ ))} +
+
+ ); +} diff --git a/website/app/components/dashboard/DonutChart.tsx b/website/app/components/dashboard/DonutChart.tsx new file mode 100644 index 00000000..f9e342c2 --- /dev/null +++ b/website/app/components/dashboard/DonutChart.tsx @@ -0,0 +1,141 @@ +"use client"; + +import { useState } from "react"; + +export interface CategoryData { + label: string; + value: number; + color: string; +} + +interface DonutChartProps { + data: CategoryData[]; + size?: number; + strokeWidth?: number; +} + +const SEGMENT_GAP = 4; + +export default function DonutChart({ data, size = 216, strokeWidth = 20 }: DonutChartProps) { + const [hovered, setHovered] = useState(null); + + const center = size / 2; + const radius = (size - strokeWidth) / 2; + const circumference = 2 * Math.PI * radius; + const total = data.reduce((sum, d) => sum + d.value, 0); + + let cumulativeAngle = -90; + + const segments = data.map((item) => { + const percent = item.value / total; + const dashLength = Math.max(0, percent * circumference - SEGMENT_GAP); + const rotation = cumulativeAngle; + cumulativeAngle += percent * 360; + return { ...item, dashLength, rotation, percent }; + }); + + const activeSegment = hovered ? segments.find((s) => s.label === hovered) : null; + + return ( +
+
+ + {/* Background track */} + + {segments.map((seg) => ( + setHovered(seg.label)} + onMouseLeave={() => setHovered(null)} + /> + ))} + + + {/* Center label */} +
+ {activeSegment ? ( + <> + + {activeSegment.value}% + + {activeSegment.label} + + ) : ( + <> + Today + {data.length} areas + + )} +
+
+ + {/* Legend */} +
+ {segments.map((seg) => ( +
setHovered(seg.label)} + onMouseLeave={() => setHovered(null)} + > +
+
+ + + {seg.label} + +
+ + {seg.value}% + +
+ {/* Mini progress bar */} +
+
+
+
+ ))} +
+
+ ); +} diff --git a/website/app/components/dashboard/GoalCard.tsx b/website/app/components/dashboard/GoalCard.tsx new file mode 100644 index 00000000..caa77484 --- /dev/null +++ b/website/app/components/dashboard/GoalCard.tsx @@ -0,0 +1,153 @@ +"use client"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export type GoalCategory = "Work" | "Health" | "Relationships"; + +export interface Goal { + id: string; + name: string; + category: GoalCategory; + streak: number; + completionRate: number; // 0–100 + frequency: string; + icon: string; + recentDays: boolean[]; // last 7 days — true = completed +} + +// ─── Category config (mirrors donut chart + activity list colors) ───────────── + +const CATEGORY_CONFIG: Record< + GoalCategory, + { badge: string; barColor: string; glowColor: string } +> = { + Work: { + badge: "bg-blue-500/15 text-blue-300", + barColor: "#3b82f6", + glowColor: "rgba(59,130,246,0.15)", + }, + Health: { + badge: "bg-emerald-500/15 text-emerald-300", + barColor: "#10b981", + glowColor: "rgba(16,185,129,0.15)", + }, + Relationships: { + badge: "bg-amber-500/15 text-amber-300", + barColor: "#f59e0b", + glowColor: "rgba(245,158,11,0.15)", + }, +}; + +// ─── Sub-components ─────────────────────────────────────────────────────────── + +function StreakBadge({ streak }: { streak: number }) { + if (streak === 0) { + return No streak yet; + } + const isHot = streak >= 10; + return ( + + 🔥{" "} + + {streak} + {" "} + day{streak !== 1 ? "s" : ""} + {isHot && 🏆} + + ); +} + +function RecentDays({ + days, + barColor, +}: { + days: boolean[]; + barColor: string; +}) { + return ( +
+ 7d + {days.map((done, i) => ( +
+ ))} +
+ ); +} + +// ─── GoalCard ───────────────────────────────────────────────────────────────── + +interface GoalCardProps { + goal: Goal; +} + +export default function GoalCard({ goal }: GoalCardProps) { + const config = CATEGORY_CONFIG[goal.category]; + const isStruggling = goal.completionRate < 40; + const barColor = isStruggling ? "#f43f5e" : config.barColor; + + return ( +
+ {/* Colored top-edge accent — animates in on hover */} +
+ + {/* Row 1: icon + name + category tag */} +
+
+ {goal.icon} +
+

{goal.name}

+

{goal.frequency}

+
+
+ + {goal.category} + +
+ + {/* Row 2: completion rate progress bar */} +
+
+ Completion + + {goal.completionRate}% + +
+
+
+
+
+ + {/* Row 3: streak + recent days dots */} +
+ + +
+
+ ); +} diff --git a/website/app/components/dashboard/GoalsSection.tsx b/website/app/components/dashboard/GoalsSection.tsx new file mode 100644 index 00000000..0b2ec39e --- /dev/null +++ b/website/app/components/dashboard/GoalsSection.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState, useMemo } from "react"; +import GoalCard, { Goal } from "./GoalCard"; + +// ─── Mock data ──────────────────────────────────────────────────────────────── + +const MOCK_GOALS: Goal[] = [ + { + id: "1", + name: "Go to gym", + category: "Health", + streak: 5, + completionRate: 80, + frequency: "3x per week", + icon: "🏋️", + recentDays: [true, true, false, true, true, true, true], + }, + { + id: "2", + name: "Deep work sessions", + category: "Work", + streak: 3, + completionRate: 60, + frequency: "Daily", + icon: "💻", + recentDays: [true, false, true, false, true, true, true], + }, + { + id: "3", + name: "Read before bed", + category: "Health", + streak: 12, + completionRate: 92, + frequency: "Daily", + icon: "📚", + recentDays: [true, true, true, true, true, true, true], + }, + { + id: "4", + name: "Call a friend or family", + category: "Relationships", + streak: 1, + completionRate: 35, + frequency: "2x per week", + icon: "📞", + recentDays: [false, false, false, false, true, false, false], + }, + { + id: "5", + name: "Morning walk", + category: "Health", + streak: 0, + completionRate: 20, + frequency: "Daily", + icon: "🚶", + recentDays: [false, true, false, false, false, false, false], + }, + { + id: "6", + name: "Team check-ins", + category: "Work", + streak: 8, + completionRate: 95, + frequency: "Daily", + icon: "💼", + recentDays: [true, true, true, true, true, true, true], + }, +]; + +// ─── Sort modes ─────────────────────────────────────────────────────────────── + +type SortMode = "streak" | "needs-work"; + +const SORT_OPTIONS: { id: SortMode; label: string }[] = [ + { id: "streak", label: "🔥 Top Streaks" }, + { id: "needs-work", label: "⚠️ Needs Work" }, +]; + +function sortGoals(goals: Goal[], mode: SortMode): Goal[] { + return [...goals].sort((a, b) => + mode === "streak" + ? b.streak - a.streak + : a.completionRate - b.completionRate + ); +} + +// ─── Summary stats ──────────────────────────────────────────────────────────── + +function computeStats(goals: Goal[]) { + const active = goals.filter((g) => g.streak > 0).length; + const avgCompletion = Math.round( + goals.reduce((s, g) => s + g.completionRate, 0) / goals.length + ); + const topStreak = Math.max(...goals.map((g) => g.streak)); + const struggling = goals.filter((g) => g.completionRate < 40).length; + return { active, avgCompletion, topStreak, struggling }; +} + +// ─── GoalsSection ───────────────────────────────────────────────────────────── + +export default function GoalsSection() { + const [sortMode, setSortMode] = useState("streak"); + + const sorted = useMemo(() => sortGoals(MOCK_GOALS, sortMode), [sortMode]); + const stats = useMemo(() => computeStats(MOCK_GOALS), []); + + return ( +
+ {/* Header */} +
+
+

Goals & Progress

+

{MOCK_GOALS.length} active goals this month

+
+ + {/* Sort toggle */} +
+ {SORT_OPTIONS.map((opt) => ( + + ))} +
+
+ + {/* Goal cards grid */} +
+ {sorted.map((goal) => ( + + ))} +
+ + {/* Summary footer */} +
+
+
+

On streak

+

+ {stats.active} + / {MOCK_GOALS.length} +

+
+
+

Avg completion

+

+ {stats.avgCompletion} + % +

+
+
+

Best streak

+

+ 🔥 {stats.topStreak} + days +

+
+ {stats.struggling > 0 && ( +
+

Needs attention

+

+ {stats.struggling} + goal{stats.struggling !== 1 ? "s" : ""} +

+
+ )} +
+

last 7 days shown per goal

+
+
+ ); +} diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx new file mode 100644 index 00000000..41a78d76 --- /dev/null +++ b/website/app/components/dashboard/HabitHeatmap.tsx @@ -0,0 +1,250 @@ +"use client"; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +export interface HabitDay { + date: string; // "YYYY-MM-DD" + value: number; // 0–100 +} + +interface HabitHeatmapProps { + data?: HabitDay[]; + month?: string; // "YYYY-MM" +} + +// ─── Mock data (Feb 2026 — realistic weekday/weekend pattern) ───────────────── + +const MOCK_DATA: HabitDay[] = [ + { date: "2026-02-01", value: 30 }, // Sun + { date: "2026-02-02", value: 75 }, // Mon + { date: "2026-02-03", value: 85 }, // Tue + { date: "2026-02-04", value: 60 }, // Wed + { date: "2026-02-05", value: 90 }, // Thu + { date: "2026-02-06", value: 70 }, // Fri + { date: "2026-02-07", value: 20 }, // Sat + { date: "2026-02-08", value: 0 }, // Sun — full rest + { date: "2026-02-09", value: 80 }, // Mon + { date: "2026-02-10", value: 65 }, // Tue + { date: "2026-02-11", value: 88 }, // Wed + { date: "2026-02-12", value: 72 }, // Thu + { date: "2026-02-13", value: 55 }, // Fri + { date: "2026-02-14", value: 35 }, // Sat + { date: "2026-02-15", value: 25 }, // Sun + { date: "2026-02-16", value: 92 }, // Mon — peak + { date: "2026-02-17", value: 78 }, // Tue + { date: "2026-02-18", value: 0 }, // Wed — off day + { date: "2026-02-19", value: 45 }, // Thu — recovering + { date: "2026-02-20", value: 68 }, // Fri + { date: "2026-02-21", value: 40 }, // Sat + { date: "2026-02-22", value: 15 }, // Sun + { date: "2026-02-23", value: 82 }, // Mon — today + { date: "2026-02-24", value: 58 }, // Tue + { date: "2026-02-25", value: 76 }, // Wed + { date: "2026-02-26", value: 88 }, // Thu + { date: "2026-02-27", value: 62 }, // Fri + { date: "2026-02-28", value: 30 }, // Sat +]; + +// ─── Utilities ──────────────────────────────────────────────────────────────── + +function getIntensityColor(value: number): string { + if (value === 0) return "rgba(30,41,59,0.7)"; + if (value <= 25) return "rgba(16,185,129,0.22)"; + if (value <= 50) return "rgba(16,185,129,0.45)"; + if (value <= 75) return "rgba(16,185,129,0.68)"; + return "rgba(16,185,129,0.95)"; +} + +function formatTooltipDate(dateStr: string): string { + return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); +} + +function formatTooltipValue(value: number): string { + if (value === 0) return "No activity"; + if (value <= 25) return `${value}% — light day`; + if (value <= 50) return `${value}% — moderate`; + if (value <= 75) return `${value}% — solid`; + return `${value}% — excellent`; +} + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +const LEGEND_LEVELS = [0, 20, 45, 70, 95] as const; + +// ─── Component ──────────────────────────────────────────────────────────────── + +export default function HabitHeatmap({ + data = MOCK_DATA, + month = "2026-02", +}: HabitHeatmapProps) { + const today = new Date().toISOString().split("T")[0]; + + const [year, monthNum] = month.split("-").map(Number); + const firstDay = new Date(year, monthNum - 1, 1); + const daysInMonth = new Date(year, monthNum, 0).getDate(); + const startDayOfWeek = firstDay.getDay(); + + const monthLabel = firstDay.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + + const dataMap = new Map(data.map((d) => [d.date, d.value])); + + // Build flat cell array: leading empty + days + trailing empty + type Cell = + | { type: "empty"; key: string } + | { type: "day"; day: number; date: string; value: number }; + + const cells: Cell[] = []; + + for (let i = 0; i < startDayOfWeek; i++) { + cells.push({ type: "empty", key: `pre-${i}` }); + } + + for (let d = 1; d <= daysInMonth; d++) { + const dateStr = `${year}-${String(monthNum).padStart(2, "0")}-${String(d).padStart(2, "0")}`; + cells.push({ type: "day", day: d, date: dateStr, value: dataMap.get(dateStr) ?? 0 }); + } + + const remainder = cells.length % 7; + if (remainder !== 0) { + for (let i = 0; i < 7 - remainder; i++) { + cells.push({ type: "empty", key: `post-${i}` }); + } + } + + return ( +
+ {/* Header */} +
+
+

Monthly Habit Overview

+

{monthLabel}

+
+ + {/* Legend */} +
+ Less +
+ {LEGEND_LEVELS.map((v) => ( +
+ ))} +
+ More +
+
+ + {/* Grid */} +
+ {/* Weekday labels */} +
+ {WEEKDAYS.map((label) => ( +
+ {label} +
+ ))} +
+ + {/* Day cells */} +
+ {cells.map((cell, idx) => { + if (cell.type === "empty") { + return ( +
+ ); + } + + const isToday = cell.date === today; + + return ( +
+ {/* Cell square */} +
+ {/* Day number — subtle overlay */} + + {cell.day} + +
+ + {/* Tooltip */} +
+
+

+ {formatTooltipDate(cell.date)} + {isToday && ( + Today + )} +

+

+ {formatTooltipValue(cell.value)} +

+
+ {/* Arrow */} +
+
+
+ ); + })} +
+ + {/* Month summary row */} +
+
+
+

Active days

+

+ {data.filter((d) => d.value > 0).length} + / {daysInMonth} +

+
+
+

Avg score

+

+ {Math.round(data.filter((d) => d.value > 0).reduce((s, d) => s + d.value, 0) / data.filter((d) => d.value > 0).length)} + % +

+
+
+

Best day

+

+ {Math.max(...data.map((d) => d.value))} + % +

+
+
+ hover cells for details +
+
+
+ ); +} diff --git a/website/app/components/dashboard/TodayOverview.tsx b/website/app/components/dashboard/TodayOverview.tsx new file mode 100644 index 00000000..1d937d8e --- /dev/null +++ b/website/app/components/dashboard/TodayOverview.tsx @@ -0,0 +1,57 @@ +"use client"; + +import DonutChart, { CategoryData } from "./DonutChart"; +import ActivityList, { Activity } from "./ActivityList"; + +const MOCK_CATEGORIES: CategoryData[] = [ + { label: "Work", value: 50, color: "#3b82f6" }, + { label: "Health", value: 30, color: "#10b981" }, + { label: "Relationships", value: 20, color: "#f59e0b" }, +]; + +const MOCK_ACTIVITIES: Activity[] = [ + { id: "1", title: "Morning workout", category: "Health", time: "7:00 AM", icon: "🏋️" }, + { id: "2", title: "Team standup", category: "Work", time: "9:30 AM", icon: "💼" }, + { id: "3", title: "Deep work session", category: "Work", time: "10:00 AM", icon: "💻" }, + { id: "4", title: "Lunch with family", category: "Relationships", time: "1:00 PM", icon: "👨‍👩‍👧" }, + { id: "5", title: "Project review", category: "Work", time: "3:00 PM", icon: "📊" }, + { id: "6", title: "Evening run", category: "Health", time: "6:30 PM", icon: "🏃" }, +]; + +export default function TodayOverview() { + const today = new Date().toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + }); + + return ( +
+ {/* Card header */} +
+
+

Today's Overview

+

{today}

+
+ + Today + +
+ + {/* Card body */} +
+ {/* Left: Donut chart */} +
+

Time Distribution

+ +
+ + {/* Right: Activity list */} +
+

Today's Activities

+ +
+
+
+ ); +} diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx index c77fc8ad..9da17312 100644 --- a/website/app/dashboard/page.tsx +++ b/website/app/dashboard/page.tsx @@ -3,6 +3,10 @@ import { useUser, useClerk } from "@clerk/nextjs"; import { useRouter } from "next/navigation"; import Image from "next/image"; import Link from "next/link"; +import TodayOverview from "@/app/components/dashboard/TodayOverview"; +import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap"; +import GoalsSection from "@/app/components/dashboard/GoalsSection"; +import ActivityLogsSection from "@/app/components/dashboard/ActivityLogsSection"; import { useState, useRef, useEffect, useCallback } from "react"; interface WhatsAppSession { @@ -647,6 +651,18 @@ export default function DashboardPage() {
+ {/* Today Overview */} + + + {/* Monthly Habit Heatmap */} + + + {/* Goals & Progress */} + + + {/* Activity Logs & Classification */} + + {/* Main Content */}
{/* WhatsApp Session Detail */} diff --git a/website/app/globals.css b/website/app/globals.css index 24370d3f..9a4aa46e 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -78,6 +78,17 @@ body { } } +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + .animate-fade-in { animation: fade-in 1s ease-out; } @@ -92,6 +103,15 @@ body { animation-fill-mode: both; } +/* Dashboard section entrance animations */ +.animate-fade-in-up { + animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) both; +} +.animate-fade-in-up-1 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.05s both; } +.animate-fade-in-up-2 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.12s both; } +.animate-fade-in-up-3 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.19s both; } +.animate-fade-in-up-4 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.26s both; } + /* Scroll-triggered reveal */ .reveal { opacity: 0; From f0714222220e37a23595bf73db112388d70bbf7f Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Mon, 23 Feb 2026 21:57:05 +0500 Subject: [PATCH 03/78] feat(dashboard): add AI processing pipeline section with animated 3-step input-to-output demo --- .../app/components/dashboard/AIPipeline.tsx | 101 ++++++++++++++++++ .../components/dashboard/AIProcessingCard.tsx | 95 ++++++++++++++++ .../app/components/dashboard/RawInputCard.tsx | 54 ++++++++++ .../dashboard/StructuredOutputCard.tsx | 68 ++++++++++++ website/app/dashboard/page.tsx | 4 + website/app/globals.css | 15 +++ 6 files changed, 337 insertions(+) create mode 100644 website/app/components/dashboard/AIPipeline.tsx create mode 100644 website/app/components/dashboard/AIProcessingCard.tsx create mode 100644 website/app/components/dashboard/RawInputCard.tsx create mode 100644 website/app/components/dashboard/StructuredOutputCard.tsx diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx new file mode 100644 index 00000000..8902b1e9 --- /dev/null +++ b/website/app/components/dashboard/AIPipeline.tsx @@ -0,0 +1,101 @@ +import RawInputCard from "./RawInputCard"; +import AIProcessingCard from "./AIProcessingCard"; +import StructuredOutputCard from "./StructuredOutputCard"; + +// ─── Arrow between steps (responsive: horizontal on desktop, vertical on mobile) + +function FlowArrow() { + return ( + <> + {/* Desktop — horizontal */} +
+
+
+ + + +
+
+ {/* Mobile — vertical */} +
+ + + +
+ + ); +} + +// ─── AIPipeline ─────────────────────────────────────────────────────────────── + +export default function AIPipeline() { + return ( +
+ {/* Header */} +
+
+

AI Processing Pipeline

+

+ How raw journal input becomes structured dashboard data +

+
+ + + Live Demo + +
+ + {/* 3-step pipeline body */} +
+ + + + + +
+ + {/* Connection footer — links output to dashboard sections */} +
+
+ + + This output powers your dashboard below + +
+
+ + activities{" "} + {" "} + Activity Logs + + · + + summary{" "} + {" "} + Donut Chart + +
+
+
+ ); +} diff --git a/website/app/components/dashboard/AIProcessingCard.tsx b/website/app/components/dashboard/AIProcessingCard.tsx new file mode 100644 index 00000000..5a27f0d4 --- /dev/null +++ b/website/app/components/dashboard/AIProcessingCard.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { useEffect, useState } from "react"; + +// ─── Mock processing steps (cycles on a timer) ──────────────────────────────── + +const PROCESSING_STEPS = [ + "Identifying activity mentions...", + "Classifying by category...", + "Extracting duration metrics...", + "Building structured output...", +]; + +const CONCEPT_TAGS = [ + { label: "Health", style: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" }, + { label: "Work", style: "bg-blue-500/10 text-blue-400 border-blue-500/20" }, + { label: "Relationships", style: "bg-amber-500/10 text-amber-400 border-amber-500/20" }, + { label: "Duration", style: "bg-violet-500/10 text-violet-400 border-violet-500/20" }, +]; + +// ─── AIProcessingCard ───────────────────────────────────────────────────────── + +export default function AIProcessingCard() { + const [step, setStep] = useState(0); + + useEffect(() => { + const id = setInterval(() => setStep((s) => (s + 1) % PROCESSING_STEPS.length), 1800); + return () => clearInterval(id); + }, []); + + return ( +
+ {/* Step label */} +
+ + 2 + + + AI Extraction + +
+ + {/* Card */} +
+ {/* Brain / AI icon */} +
+ + + +
+ + {/* Pulsing indicator dots */} +
+ {[0, 150, 300].map((delay, i) => ( +
+ ))} +
+ + {/* Cycling step message */} +

+ {PROCESSING_STEPS[step]} +

+ + {/* Extracted concept tags */} +
+ {CONCEPT_TAGS.map(({ label, style }) => ( + + {label} + + ))} +
+
+
+ ); +} diff --git a/website/app/components/dashboard/RawInputCard.tsx b/website/app/components/dashboard/RawInputCard.tsx new file mode 100644 index 00000000..93519bd3 --- /dev/null +++ b/website/app/components/dashboard/RawInputCard.tsx @@ -0,0 +1,54 @@ +// ─── RawInputCard ───────────────────────────────────────────────────────────── +// Step 1 of the AI pipeline — the raw text message from the user + +const MOCK_MESSAGE = + "Had a great day. Went to the gym in the morning, worked 5 hours on the project proposal, and had dinner with my parents."; + +export default function RawInputCard() { + return ( +
+ {/* Step label */} +
+ + 1 + + + Raw Input + +
+ + {/* Card */} +
+ {/* Source row */} +
+
+ + + +
+
+

Journal · WhatsApp

+

Today · 21:03

+
+
+ + {/* Message bubble */} +
+

+ “{MOCK_MESSAGE}” +

+
+ + {/* Metadata chips */} +
+ + unstructured text + + + 3 activities detected + +
+
+
+ ); +} diff --git a/website/app/components/dashboard/StructuredOutputCard.tsx b/website/app/components/dashboard/StructuredOutputCard.tsx new file mode 100644 index 00000000..6671cbfe --- /dev/null +++ b/website/app/components/dashboard/StructuredOutputCard.tsx @@ -0,0 +1,68 @@ +// ─── Mock JSON output ───────────────────────────────────────────────────────── + +const MOCK_JSON = `{ + "activities": [ + { "text": "Morning gym", "category": "Health" }, + { "text": "5h deep work", "category": "Work", + "duration": "5h" }, + { "text": "Dinner with parents", "category": "Relationships" } + ], + "summary": { + "health": 30, + "work": 50, + "relationships": 20 + } +}`; + +// ─── Simple JSON syntax highlighter (no library) ────────────────────────────── + +function syntaxHighlight(json: string): string { + return ( + json + // Keys: "key": + .replace(/"([^"]+)"(\s*:)/g, '"$1"$2') + // String values: : "value" + .replace(/:\s*"([^"]+)"/g, ': "$1"') + // Numbers: : 30 + .replace(/:\s*(\d+)/g, ': $1') + // Braces and brackets + .replace(/[{}\[\]]/g, '$&') + ); +} + +// ─── StructuredOutputCard ───────────────────────────────────────────────────── + +export default function StructuredOutputCard() { + return ( +
+ {/* Step label */} +
+ + 3 + + + Structured Output + +
+ + {/* Card */} +
+ {/* Fake code-editor chrome */} +
+
+
+
+
+
+ output.json +
+ + {/* Highlighted JSON */} +
+      
+
+ ); +} diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx index 9da17312..0ac49aa7 100644 --- a/website/app/dashboard/page.tsx +++ b/website/app/dashboard/page.tsx @@ -3,6 +3,7 @@ import { useUser, useClerk } from "@clerk/nextjs"; import { useRouter } from "next/navigation"; import Image from "next/image"; import Link from "next/link"; +import AIPipeline from "@/app/components/dashboard/AIPipeline"; import TodayOverview from "@/app/components/dashboard/TodayOverview"; import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap"; import GoalsSection from "@/app/components/dashboard/GoalsSection"; @@ -651,6 +652,9 @@ export default function DashboardPage() {
+ {/* AI Processing Pipeline */} + + {/* Today Overview */} diff --git a/website/app/globals.css b/website/app/globals.css index 9a4aa46e..ebf330fd 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -112,6 +112,21 @@ body { .animate-fade-in-up-3 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.19s both; } .animate-fade-in-up-4 { animation: fade-in-up 0.45s cubic-bezier(0.16, 1, 0.3, 1) 0.26s both; } +/* AI Pipeline — shimmer text sweep */ +@keyframes shimmer-sweep { + 0% { background-position: -200% center; } + 100% { background-position: 200% center; } +} + +.shimmer-text { + background: linear-gradient(90deg, #475569 0%, #94a3b8 45%, #475569 100%); + background-size: 200% auto; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: shimmer-sweep 2.5s linear infinite; +} + /* Scroll-triggered reveal */ .reveal { opacity: 0; From 43d43e35751203d2a394d5a1cbd7b15f3fec6372 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 09:05:40 +0500 Subject: [PATCH 04/78] Fix division by zero in HabitHeatmap average calculation when no active days exist --- website/app/components/dashboard/HabitHeatmap.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx index 41a78d76..43b98540 100644 --- a/website/app/components/dashboard/HabitHeatmap.tsx +++ b/website/app/components/dashboard/HabitHeatmap.tsx @@ -230,7 +230,12 @@ export default function HabitHeatmap({

Avg score

- {Math.round(data.filter((d) => d.value > 0).reduce((s, d) => s + d.value, 0) / data.filter((d) => d.value > 0).length)} + {(() => { + const activeDays = data.filter((d) => d.value > 0); + return activeDays.length > 0 + ? Math.round(activeDays.reduce((s, d) => s + d.value, 0) / activeDays.length) + : 0; + })()} %

From 901259b965b5848408d3a659d89a16910ae76441 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 09:11:18 +0500 Subject: [PATCH 05/78] Fix lint errors: refactor DonutChart variable reassignment, suppress intentional unused vars --- .../app/components/dashboard/DonutChart.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/website/app/components/dashboard/DonutChart.tsx b/website/app/components/dashboard/DonutChart.tsx index f9e342c2..34e8b63a 100644 --- a/website/app/components/dashboard/DonutChart.tsx +++ b/website/app/components/dashboard/DonutChart.tsx @@ -24,15 +24,16 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut const circumference = 2 * Math.PI * radius; const total = data.reduce((sum, d) => sum + d.value, 0); - let cumulativeAngle = -90; - - const segments = data.map((item) => { - const percent = item.value / total; - const dashLength = Math.max(0, percent * circumference - SEGMENT_GAP); - const rotation = cumulativeAngle; - cumulativeAngle += percent * 360; - return { ...item, dashLength, rotation, percent }; - }); + const segments = data.reduce>( + (acc, item) => { + const percent = item.value / total; + const dashLength = Math.max(0, percent * circumference - SEGMENT_GAP); + const prevAngle = acc.length > 0 ? acc[acc.length - 1].rotation + acc[acc.length - 1].percent * 360 : -90; + acc.push({ ...item, dashLength, rotation: prevAngle, percent }); + return acc; + }, + [] + ); const activeSegment = hovered ? segments.find((s) => s.label === hovered) : null; From 50080ddf05a1e5be914def72f98b965fd6bd8c5f Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 09:19:09 +0500 Subject: [PATCH 06/78] Rename "Avg score" and "Best day" labels to "Avg." and "Best" in habit heatmap --- website/app/components/dashboard/HabitHeatmap.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx index 43b98540..179f8ffb 100644 --- a/website/app/components/dashboard/HabitHeatmap.tsx +++ b/website/app/components/dashboard/HabitHeatmap.tsx @@ -228,7 +228,7 @@ export default function HabitHeatmap({

-

Avg score

+

Avg.

{(() => { const activeDays = data.filter((d) => d.value > 0); @@ -240,7 +240,7 @@ export default function HabitHeatmap({

-

Best day

+

Best

{Math.max(...data.map((d) => d.value))} % From 40831193112c814bc717d7cdd489a8daa80bfbcb Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 09:52:34 +0500 Subject: [PATCH 07/78] Polish monthly habit heatmap with responsive scaling, streak emphasis, and calendar-grade visual clarity. --- .../app/components/dashboard/HabitHeatmap.tsx | 479 ++++++++++++------ 1 file changed, 328 insertions(+), 151 deletions(-) diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx index 179f8ffb..2d092178 100644 --- a/website/app/components/dashboard/HabitHeatmap.tsx +++ b/website/app/components/dashboard/HabitHeatmap.tsx @@ -1,4 +1,5 @@ "use client"; +import { useState } from "react"; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -12,47 +13,59 @@ interface HabitHeatmapProps { month?: string; // "YYYY-MM" } -// ─── Mock data (Feb 2026 — realistic weekday/weekend pattern) ───────────────── +interface StatsPanelProps { + activeDays: number; + daysInMonth: number; + avgPercent: number; + bestValue: number; + bestDayLabel: string; + currentStreak: number; +} + +// ─── Mock data (Jan 2026 — 31-day max layout test) ───────────────────────────── const MOCK_DATA: HabitDay[] = [ - { date: "2026-02-01", value: 30 }, // Sun - { date: "2026-02-02", value: 75 }, // Mon - { date: "2026-02-03", value: 85 }, // Tue - { date: "2026-02-04", value: 60 }, // Wed - { date: "2026-02-05", value: 90 }, // Thu - { date: "2026-02-06", value: 70 }, // Fri - { date: "2026-02-07", value: 20 }, // Sat - { date: "2026-02-08", value: 0 }, // Sun — full rest - { date: "2026-02-09", value: 80 }, // Mon - { date: "2026-02-10", value: 65 }, // Tue - { date: "2026-02-11", value: 88 }, // Wed - { date: "2026-02-12", value: 72 }, // Thu - { date: "2026-02-13", value: 55 }, // Fri - { date: "2026-02-14", value: 35 }, // Sat - { date: "2026-02-15", value: 25 }, // Sun - { date: "2026-02-16", value: 92 }, // Mon — peak - { date: "2026-02-17", value: 78 }, // Tue - { date: "2026-02-18", value: 0 }, // Wed — off day - { date: "2026-02-19", value: 45 }, // Thu — recovering - { date: "2026-02-20", value: 68 }, // Fri - { date: "2026-02-21", value: 40 }, // Sat - { date: "2026-02-22", value: 15 }, // Sun - { date: "2026-02-23", value: 82 }, // Mon — today - { date: "2026-02-24", value: 58 }, // Tue - { date: "2026-02-25", value: 76 }, // Wed - { date: "2026-02-26", value: 88 }, // Thu - { date: "2026-02-27", value: 62 }, // Fri - { date: "2026-02-28", value: 30 }, // Sat + { date: "2026-01-01", value: 70 }, // Thu + { date: "2026-01-02", value: 82 }, // Fri + { date: "2026-01-03", value: 35 }, // Sat + { date: "2026-01-04", value: 20 }, // Sun + { date: "2026-01-05", value: 76 }, // Mon + { date: "2026-01-06", value: 88 }, // Tue + { date: "2026-01-07", value: 64 }, // Wed + { date: "2026-01-08", value: 90 }, // Thu + { date: "2026-01-09", value: 72 }, // Fri + { date: "2026-01-10", value: 28 }, // Sat + { date: "2026-01-11", value: 0 }, // Sun — reset + { date: "2026-01-12", value: 68 }, // Mon + { date: "2026-01-13", value: 80 }, // Tue + { date: "2026-01-14", value: 92 }, // Wed — peak + { date: "2026-01-15", value: 74 }, // Thu + { date: "2026-01-16", value: 58 }, // Fri + { date: "2026-01-17", value: 32 }, // Sat + { date: "2026-01-18", value: 18 }, // Sun + { date: "2026-01-19", value: 84 }, // Mon + { date: "2026-01-20", value: 66 }, // Tue + { date: "2026-01-21", value: 0 }, // Wed — off day + { date: "2026-01-22", value: 52 }, // Thu + { date: "2026-01-23", value: 74 }, // Fri + { date: "2026-01-24", value: 40 }, // Sat + { date: "2026-01-25", value: 22 }, // Sun + { date: "2026-01-26", value: 86 }, // Mon + { date: "2026-01-27", value: 79 }, // Tue + { date: "2026-01-28", value: 67 }, // Wed + { date: "2026-01-29", value: 83 }, // Thu + { date: "2026-01-30", value: 71 }, // Fri + { date: "2026-01-31", value: 36 }, // Sat ]; // ─── Utilities ──────────────────────────────────────────────────────────────── function getIntensityColor(value: number): string { - if (value === 0) return "rgba(30,41,59,0.7)"; - if (value <= 25) return "rgba(16,185,129,0.22)"; - if (value <= 50) return "rgba(16,185,129,0.45)"; - if (value <= 75) return "rgba(16,185,129,0.68)"; - return "rgba(16,185,129,0.95)"; + if (value === 0) return "rgba(39,39,42,0.82)"; + if (value <= 25) return "rgba(34,197,94,0.24)"; + if (value <= 50) return "rgba(34,197,94,0.4)"; + if (value <= 75) return "rgba(34,197,94,0.56)"; + return "rgba(34,197,94,0.7)"; } function formatTooltipDate(dateStr: string): string { @@ -64,11 +77,14 @@ function formatTooltipDate(dateStr: string): string { } function formatTooltipValue(value: number): string { - if (value === 0) return "No activity"; - if (value <= 25) return `${value}% — light day`; - if (value <= 50) return `${value}% — moderate`; - if (value <= 75) return `${value}% — solid`; - return `${value}% — excellent`; + return `${value}% productive`; +} + +function formatShortMonthDay(dateStr: string): string { + return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); } // ─── Constants ──────────────────────────────────────────────────────────────── @@ -76,13 +92,82 @@ function formatTooltipValue(value: number): string { const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const LEGEND_LEVELS = [0, 20, 45, 70, 95] as const; +function StatsPanel({ + activeDays, + daysInMonth, + avgPercent, + bestValue, + bestDayLabel, + currentStreak, +}: StatsPanelProps) { + return ( +

+ ); +} + // ─── Component ──────────────────────────────────────────────────────────────── export default function HabitHeatmap({ data = MOCK_DATA, - month = "2026-02", + month = "2026-01", }: HabitHeatmapProps) { const today = new Date().toISOString().split("T")[0]; + const [hoveredStreakId, setHoveredStreakId] = useState(null); const [year, monthNum] = month.split("-").map(Number); const firstDay = new Date(year, monthNum - 1, 1); @@ -95,11 +180,92 @@ export default function HabitHeatmap({ }); const dataMap = new Map(data.map((d) => [d.date, d.value])); + const sortedData = [...data].sort((a, b) => a.date.localeCompare(b.date)); + const activeDays = data.filter((d) => d.value > 0).length; + const avgPercent = + activeDays > 0 + ? Math.round(data.filter((d) => d.value > 0).reduce((sum, d) => sum + d.value, 0) / activeDays) + : 0; + const bestDay = + sortedData.length > 0 + ? sortedData.reduce((best, day) => (day.value > best.value ? day : best), sortedData[0]) + : null; + + const monthDays = Array.from({ length: daysInMonth }, (_, i) => { + const day = i + 1; + const date = `${year}-${String(monthNum).padStart(2, "0")}-${String(day).padStart(2, "0")}`; + return { day, date, value: dataMap.get(date) ?? 0 }; + }); + + type StreakMeta = { + streakId: number | null; + isStreak: boolean; + isCurrentStreak: boolean; + }; + + const streakMetaByDate = new Map(); + monthDays.forEach((entry) => + streakMetaByDate.set(entry.date, { + streakId: null, + isStreak: false, + isCurrentStreak: false, + }), + ); + + let streakCounter = 0; + let i = 0; + while (i < monthDays.length) { + if (monthDays[i].value <= 0) { + i += 1; + continue; + } + const start = i; + while (i < monthDays.length && monthDays[i].value > 0) i += 1; + const end = i - 1; + const runLength = end - start + 1; + const streakId = runLength >= 2 ? streakCounter++ : null; + for (let idx = start; idx <= end; idx++) { + streakMetaByDate.set(monthDays[idx].date, { + streakId, + isStreak: runLength >= 2, + isCurrentStreak: false, + }); + } + } + + let currentStreak = 0; + let currentEnd = monthDays.length - 1; + while (currentEnd >= 0 && monthDays[currentEnd].value <= 0) currentEnd -= 1; + if (currentEnd >= 0) { + let currentStart = currentEnd; + while (currentStart >= 0 && monthDays[currentStart].value > 0) currentStart -= 1; + currentStart += 1; + currentStreak = currentEnd - currentStart + 1; + for (let idx = currentStart; idx <= currentEnd; idx++) { + const prevMeta = streakMetaByDate.get(monthDays[idx].date) ?? { + streakId: null, + isStreak: false, + isCurrentStreak: false, + }; + streakMetaByDate.set(monthDays[idx].date, { + ...prevMeta, + isCurrentStreak: true, + }); + } + } // Build flat cell array: leading empty + days + trailing empty type Cell = | { type: "empty"; key: string } - | { type: "day"; day: number; date: string; value: number }; + | { + type: "day"; + day: number; + date: string; + value: number; + streakId: number | null; + isStreak: boolean; + isCurrentStreak: boolean; + }; const cells: Cell[] = []; @@ -109,7 +275,20 @@ export default function HabitHeatmap({ for (let d = 1; d <= daysInMonth; d++) { const dateStr = `${year}-${String(monthNum).padStart(2, "0")}-${String(d).padStart(2, "0")}`; - cells.push({ type: "day", day: d, date: dateStr, value: dataMap.get(dateStr) ?? 0 }); + const streakMeta = streakMetaByDate.get(dateStr) ?? { + streakId: null, + isStreak: false, + isCurrentStreak: false, + }; + cells.push({ + type: "day", + day: d, + date: dateStr, + value: dataMap.get(dateStr) ?? 0, + streakId: streakMeta.streakId, + isStreak: streakMeta.isStreak, + isCurrentStreak: streakMeta.isCurrentStreak, + }); } const remainder = cells.length % 7; @@ -122,132 +301,130 @@ export default function HabitHeatmap({ return (
{/* Header */} -
+
-

Monthly Habit Overview

-

{monthLabel}

-
- - {/* Legend */} -
- Less -
- {LEGEND_LEVELS.map((v) => ( -
- ))} -
- More +

Monthly Habit Overview

+

{monthLabel}

{/* Grid */} -
- {/* Weekday labels */} -
- {WEEKDAYS.map((label) => ( -
- {label} +
+
+
+
+ Less +
+ {LEGEND_LEVELS.map((v) => ( +
+ ))} +
+ More
- ))} -
- {/* Day cells */} -
- {cells.map((cell, idx) => { - if (cell.type === "empty") { - return ( + {/* Weekday labels */} +
+ {WEEKDAYS.map((label) => (
- ); - } + key={label} + className="text-center text-[11px] font-medium text-white/50 py-0 select-none" + > + {label} +
+ ))} +
- const isToday = cell.date === today; + {/* Day cells */} +
+ {cells.map((cell, idx) => { + if (cell.type === "empty") { + return ( +
+ ); + } - return ( -
- {/* Cell square */} -
setHoveredStreakId(cell.streakId)} + onMouseLeave={() => setHoveredStreakId(null)} + > + {/* Cell square */} +
- {/* Day number — subtle overlay */} - - {cell.day} - -
+ style={{ backgroundColor: getIntensityColor(cell.value) }} + > + {/* Day number — subtle overlay */} + + {cell.day} + + {cell.isStreak && ( + + )} +
- {/* Tooltip */} -
-
-

- {formatTooltipDate(cell.date)} - {isToday && ( - Today - )} -

-

- {formatTooltipValue(cell.value)} -

-
- {/* Arrow */} -
+

+ {formatTooltipDate(cell.date)} + {isToday && ( + Today + )} +

+

+ {formatTooltipValue(cell.value)} +

+
+ {/* Arrow */} +
-
-
- ); - })} -
- - {/* Month summary row */} -
-
-
-

Active days

-

- {data.filter((d) => d.value > 0).length} - / {daysInMonth} -

-
-
-

Avg.

-

- {(() => { - const activeDays = data.filter((d) => d.value > 0); - return activeDays.length > 0 - ? Math.round(activeDays.reduce((s, d) => s + d.value, 0) / activeDays.length) - : 0; - })()} - % -

-
-
-

Best

-

- {Math.max(...data.map((d) => d.value))} - % -

+ /> +
+
+ ); + })}
- hover cells for details + +
+ +
From 58a5294a847cf505ed925b9ee6ef973dac4306c1 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 10:04:29 +0500 Subject: [PATCH 08/78] feat(dashboard): replace structured JSON output with animated live dashboard mapping preview --- .../app/components/dashboard/AIPipeline.tsx | 17 +++- .../dashboard/AnimatedActivityList.tsx | 45 ++++++++++ .../components/dashboard/AnimatedHeatmap.tsx | 39 ++++++++ .../dashboard/AnimatedProgressBar.tsx | 27 ++++++ .../components/dashboard/DashboardPreview.tsx | 38 ++++++++ .../app/components/dashboard/RawInputCard.tsx | 40 ++++++++- .../dashboard/StructuredOutputCard.tsx | 90 +++++++++++-------- 7 files changed, 254 insertions(+), 42 deletions(-) create mode 100644 website/app/components/dashboard/AnimatedActivityList.tsx create mode 100644 website/app/components/dashboard/AnimatedHeatmap.tsx create mode 100644 website/app/components/dashboard/AnimatedProgressBar.tsx create mode 100644 website/app/components/dashboard/DashboardPreview.tsx diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx index 8902b1e9..69266e69 100644 --- a/website/app/components/dashboard/AIPipeline.tsx +++ b/website/app/components/dashboard/AIPipeline.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { useEffect, useState } from "react"; import RawInputCard from "./RawInputCard"; import AIProcessingCard from "./AIProcessingCard"; import StructuredOutputCard from "./StructuredOutputCard"; @@ -49,6 +52,16 @@ function FlowArrow() { // ─── AIPipeline ─────────────────────────────────────────────────────────────── export default function AIPipeline() { + const [activeMappingStep, setActiveMappingStep] = useState(0); + + useEffect(() => { + const id = setInterval(() => { + setActiveMappingStep((prev) => (prev + 1) % 4); + }, 1800); + + return () => clearInterval(id); + }, []); + return (
{/* Header */} @@ -67,11 +80,11 @@ export default function AIPipeline() { {/* 3-step pipeline body */}
- + - +
{/* Connection footer — links output to dashboard sections */} diff --git a/website/app/components/dashboard/AnimatedActivityList.tsx b/website/app/components/dashboard/AnimatedActivityList.tsx new file mode 100644 index 00000000..3536a34e --- /dev/null +++ b/website/app/components/dashboard/AnimatedActivityList.tsx @@ -0,0 +1,45 @@ +"use client"; + +type AnimatedActivityListProps = { + activeMappingStep: number; +}; + +const BASE_ITEMS = [ + "Reviewed sprint plan", + "Quick walk after lunch", +]; + +export default function AnimatedActivityList({ activeMappingStep }: AnimatedActivityListProps) { + const showNewItem = activeMappingStep >= 3; + + return ( +
+
+

Activity Logs

+ Live +
+ +
+ {BASE_ITEMS.map((item) => ( +
+ {item} +
+ ))} + +
+ Dinner with parents +
+
+
+ ); +} diff --git a/website/app/components/dashboard/AnimatedHeatmap.tsx b/website/app/components/dashboard/AnimatedHeatmap.tsx new file mode 100644 index 00000000..6fad305f --- /dev/null +++ b/website/app/components/dashboard/AnimatedHeatmap.tsx @@ -0,0 +1,39 @@ +"use client"; + +type AnimatedHeatmapProps = { + activeMappingStep: number; +}; + +const CELLS = 14; +const ACTIVE_INDICES = [1, 2, 4, 5, 8, 10, 11]; + +export default function AnimatedHeatmap({ activeMappingStep }: AnimatedHeatmapProps) { + const isActive = activeMappingStep >= 1; + + return ( +
+
+

Health Heatmap

+ Weekly +
+ +
+ {Array.from({ length: CELLS }).map((_, index) => { + const shouldLight = isActive && ACTIVE_INDICES.includes(index); + + return ( +
+ ); + })} +
+
+ ); +} diff --git a/website/app/components/dashboard/AnimatedProgressBar.tsx b/website/app/components/dashboard/AnimatedProgressBar.tsx new file mode 100644 index 00000000..600c4fda --- /dev/null +++ b/website/app/components/dashboard/AnimatedProgressBar.tsx @@ -0,0 +1,27 @@ +"use client"; + +type AnimatedProgressBarProps = { + activeMappingStep: number; +}; + +export default function AnimatedProgressBar({ activeMappingStep }: AnimatedProgressBarProps) { + const value = activeMappingStep >= 2 ? 50 : 0; + + return ( +
+
+

Work Progress

+ {value}% +
+ +
+
+
+ +

"5h deep work" mapped to today's work target

+
+ ); +} diff --git a/website/app/components/dashboard/DashboardPreview.tsx b/website/app/components/dashboard/DashboardPreview.tsx new file mode 100644 index 00000000..404b627d --- /dev/null +++ b/website/app/components/dashboard/DashboardPreview.tsx @@ -0,0 +1,38 @@ +"use client"; + +import AnimatedHeatmap from "./AnimatedHeatmap"; +import AnimatedProgressBar from "./AnimatedProgressBar"; +import AnimatedActivityList from "./AnimatedActivityList"; + +type DashboardPreviewProps = { + activeMappingStep: number; +}; + +const STEP_COPY = [ + 'Highlighting "Morning gym" in your raw input', + "Mapping it into Health and updating the weekly heatmap", + 'Mapping "5h deep work" into your Work progress', + 'Adding "Dinner with parents" to Activity Logs', +]; + +export default function DashboardPreview({ activeMappingStep }: DashboardPreviewProps) { + return ( +
+

+ How this data updates your dashboard +

+ +
+

+ {STEP_COPY[activeMappingStep]} +

+
+ +
+ + + +
+
+ ); +} diff --git a/website/app/components/dashboard/RawInputCard.tsx b/website/app/components/dashboard/RawInputCard.tsx index 93519bd3..d67fd234 100644 --- a/website/app/components/dashboard/RawInputCard.tsx +++ b/website/app/components/dashboard/RawInputCard.tsx @@ -4,7 +4,43 @@ const MOCK_MESSAGE = "Had a great day. Went to the gym in the morning, worked 5 hours on the project proposal, and had dinner with my parents."; -export default function RawInputCard() { +const ACTIVE_PHRASES = [ + "gym in the morning", + "gym in the morning", + "worked 5 hours", + "dinner with my parents", +]; + +type RawInputCardProps = { + activeMappingStep?: number; +}; + +function renderHighlightedMessage(activeMappingStep = 0) { + const phrase = ACTIVE_PHRASES[activeMappingStep] ?? ACTIVE_PHRASES[0]; + const phraseIndex = MOCK_MESSAGE.toLowerCase().indexOf(phrase.toLowerCase()); + + if (phraseIndex === -1) { + return MOCK_MESSAGE; + } + + const before = MOCK_MESSAGE.slice(0, phraseIndex); + const match = MOCK_MESSAGE.slice(phraseIndex, phraseIndex + phrase.length); + const after = MOCK_MESSAGE.slice(phraseIndex + phrase.length); + + return ( + <> + {before} + + {match} + + {after} + + ); +} + +export default function RawInputCard({ activeMappingStep = 0 }: RawInputCardProps) { return (
{/* Step label */} @@ -35,7 +71,7 @@ export default function RawInputCard() { {/* Message bubble */}

- “{MOCK_MESSAGE}” + “{renderHighlightedMessage(activeMappingStep)}”

diff --git a/website/app/components/dashboard/StructuredOutputCard.tsx b/website/app/components/dashboard/StructuredOutputCard.tsx index 6671cbfe..4ab1a4f6 100644 --- a/website/app/components/dashboard/StructuredOutputCard.tsx +++ b/website/app/components/dashboard/StructuredOutputCard.tsx @@ -1,38 +1,29 @@ -// ─── Mock JSON output ───────────────────────────────────────────────────────── +"use client"; + +import { useMemo, useState } from "react"; +import DashboardPreview from "./DashboardPreview"; + +type StructuredOutputCardProps = { + activeMappingStep: number; +}; const MOCK_JSON = `{ "activities": [ - { "text": "Morning gym", "category": "Health" }, - { "text": "5h deep work", "category": "Work", - "duration": "5h" }, + { "text": "Morning gym", "category": "Health" }, + { "text": "5h deep work", "category": "Work", "duration": "5h" }, { "text": "Dinner with parents", "category": "Relationships" } ], - "summary": { - "health": 30, - "work": 50, - "relationships": 20 - } + "summary": { "health": 30, "work": 50, "relationships": 20 } }`; -// ─── Simple JSON syntax highlighter (no library) ────────────────────────────── +export default function StructuredOutputCard({ activeMappingStep }: StructuredOutputCardProps) { + const [viewMode, setViewMode] = useState<"user" | "developer">("user"); -function syntaxHighlight(json: string): string { - return ( - json - // Keys: "key": - .replace(/"([^"]+)"(\s*:)/g, '"$1"$2') - // String values: : "value" - .replace(/:\s*"([^"]+)"/g, ': "$1"') - // Numbers: : 30 - .replace(/:\s*(\d+)/g, ': $1') - // Braces and brackets - .replace(/[{}\[\]]/g, '$&') + const stepTitle = useMemo( + () => (viewMode === "user" ? "Dashboard Update" : "Structured Output"), + [viewMode], ); -} - -// ─── StructuredOutputCard ───────────────────────────────────────────────────── -export default function StructuredOutputCard() { return (
{/* Step label */} @@ -41,27 +32,50 @@ export default function StructuredOutputCard() { 3 - Structured Output + {stepTitle}
{/* Card */}
- {/* Fake code-editor chrome */} -
-
-
-
-
+
+

+ {viewMode === "user" ? "Live Preview" : "Developer View"} +

+ +
+ +
- output.json
- {/* Highlighted JSON */} -
+        {viewMode === "user" ? (
+          
+        ) : (
+          
+            {MOCK_JSON}
+          
+ )}
); From 1e44d43f167ddd533b9e171fb4b08be87bffd778 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 10:20:50 +0500 Subject: [PATCH 09/78] feat(features): move AI pipeline demo to features page and remove it from dashboard --- .../app/components/dashboard/AIPipeline.tsx | 26 ++++++-- .../dashboard/StructuredOutputCard.tsx | 65 +++++++++++-------- .../components/sections/AIPipelineDemo.tsx | 47 ++++++++++++++ website/app/dashboard/page.tsx | 4 -- website/app/features/page.tsx | 3 + 5 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 website/app/components/sections/AIPipelineDemo.tsx diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx index 69266e69..d5f38c57 100644 --- a/website/app/components/dashboard/AIPipeline.tsx +++ b/website/app/components/dashboard/AIPipeline.tsx @@ -5,6 +5,12 @@ import RawInputCard from "./RawInputCard"; import AIProcessingCard from "./AIProcessingCard"; import StructuredOutputCard from "./StructuredOutputCard"; +type AIPipelineProps = { + isActive?: boolean; + allowDeveloperView?: boolean; + className?: string; +}; + // ─── Arrow between steps (responsive: horizontal on desktop, vertical on mobile) function FlowArrow() { @@ -51,19 +57,28 @@ function FlowArrow() { // ─── AIPipeline ─────────────────────────────────────────────────────────────── -export default function AIPipeline() { +export default function AIPipeline({ + isActive = true, + allowDeveloperView = true, + className = "", +}: AIPipelineProps) { const [activeMappingStep, setActiveMappingStep] = useState(0); useEffect(() => { + if (!isActive) { + setActiveMappingStep(0); + return; + } + const id = setInterval(() => { setActiveMappingStep((prev) => (prev + 1) % 4); }, 1800); return () => clearInterval(id); - }, []); + }, [isActive]); return ( -
+
{/* Header */}
@@ -84,7 +99,10 @@ export default function AIPipeline() { - +
{/* Connection footer — links output to dashboard sections */} diff --git a/website/app/components/dashboard/StructuredOutputCard.tsx b/website/app/components/dashboard/StructuredOutputCard.tsx index 4ab1a4f6..73ff231c 100644 --- a/website/app/components/dashboard/StructuredOutputCard.tsx +++ b/website/app/components/dashboard/StructuredOutputCard.tsx @@ -5,6 +5,7 @@ import DashboardPreview from "./DashboardPreview"; type StructuredOutputCardProps = { activeMappingStep: number; + allowDeveloperView?: boolean; }; const MOCK_JSON = `{ @@ -16,12 +17,18 @@ const MOCK_JSON = `{ "summary": { "health": 30, "work": 50, "relationships": 20 } }`; -export default function StructuredOutputCard({ activeMappingStep }: StructuredOutputCardProps) { +export default function StructuredOutputCard({ + activeMappingStep, + allowDeveloperView = true, +}: StructuredOutputCardProps) { const [viewMode, setViewMode] = useState<"user" | "developer">("user"); const stepTitle = useMemo( - () => (viewMode === "user" ? "Dashboard Update" : "Structured Output"), - [viewMode], + () => { + if (!allowDeveloperView) return "Dashboard Update"; + return viewMode === "user" ? "Dashboard Update" : "Structured Output"; + }, + [allowDeveloperView, viewMode], ); return ( @@ -43,33 +50,35 @@ export default function StructuredOutputCard({ activeMappingStep }: StructuredOu {viewMode === "user" ? "Live Preview" : "Developer View"}

-
- - -
+ {allowDeveloperView && ( +
+ + +
+ )}
- {viewMode === "user" ? ( + {viewMode === "user" || !allowDeveloperView ? ( ) : (
diff --git a/website/app/components/sections/AIPipelineDemo.tsx b/website/app/components/sections/AIPipelineDemo.tsx
new file mode 100644
index 00000000..4f02b94d
--- /dev/null
+++ b/website/app/components/sections/AIPipelineDemo.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import AIPipeline from "../dashboard/AIPipeline";
+
+export default function AIPipelineDemo() {
+  const sectionRef = useRef(null);
+  const [isInView, setIsInView] = useState(false);
+
+  useEffect(() => {
+    const node = sectionRef.current;
+    if (!node) return;
+
+    const observer = new IntersectionObserver(
+      (entries) => {
+        entries.forEach((entry) => {
+          if (entry.isIntersecting) {
+            setIsInView(true);
+          } else if (entry.intersectionRatio === 0) {
+            setIsInView(false);
+          }
+        });
+      },
+      { threshold: 0.25 },
+    );
+
+    observer.observe(node);
+    return () => observer.disconnect();
+  }, []);
+
+  return (
+    
+
+

From journal to insights

+

+ Write naturally. We extract, classify, and visualize your habits automatically. +

+
+ + +
+ ); +} diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx index 0ac49aa7..9da17312 100644 --- a/website/app/dashboard/page.tsx +++ b/website/app/dashboard/page.tsx @@ -3,7 +3,6 @@ import { useUser, useClerk } from "@clerk/nextjs"; import { useRouter } from "next/navigation"; import Image from "next/image"; import Link from "next/link"; -import AIPipeline from "@/app/components/dashboard/AIPipeline"; import TodayOverview from "@/app/components/dashboard/TodayOverview"; import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap"; import GoalsSection from "@/app/components/dashboard/GoalsSection"; @@ -652,9 +651,6 @@ export default function DashboardPage() {
- {/* AI Processing Pipeline */} - - {/* Today Overview */} diff --git a/website/app/features/page.tsx b/website/app/features/page.tsx index a07ca02f..4bcc70be 100644 --- a/website/app/features/page.tsx +++ b/website/app/features/page.tsx @@ -2,6 +2,7 @@ import React from "react"; import Image from "next/image"; import Link from "next/link"; +import AIPipelineDemo from "../components/sections/AIPipelineDemo"; // Channel icons as SVG components const ChannelIcons = { @@ -303,6 +304,8 @@ export default function FeaturesPage() {
+ + {/* Category Comparison Table */}
From 7f12116e2cf1c7d8aa3bbf6afbf31033d210384a Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 14:32:55 +0500 Subject: [PATCH 10/78] Improve dashboard-to-logs navigation with contextual links, breadcrumbs, and highlighted log context --- .../app/components/dashboard/ActivityList.tsx | 38 ++++--- .../app/components/dashboard/DonutChart.tsx | 11 +- .../components/dashboard/TodayOverview.tsx | 31 +++++- .../components/logs/ClassificationPanel.tsx | 47 ++++++++ website/app/components/logs/LogItem.tsx | 38 +++++++ .../app/components/logs/LogsControlBar.tsx | 83 ++++++++++++++ website/app/components/logs/LogsList.tsx | 41 +++++++ website/app/components/logs/LogsPage.tsx | 91 ++++++++++++++++ website/app/components/logs/mockData.ts | 101 ++++++++++++++++++ website/app/dashboard/page.tsx | 16 ++- website/app/logs/page.tsx | 7 ++ 11 files changed, 480 insertions(+), 24 deletions(-) create mode 100644 website/app/components/logs/ClassificationPanel.tsx create mode 100644 website/app/components/logs/LogItem.tsx create mode 100644 website/app/components/logs/LogsControlBar.tsx create mode 100644 website/app/components/logs/LogsList.tsx create mode 100644 website/app/components/logs/LogsPage.tsx create mode 100644 website/app/components/logs/mockData.ts create mode 100644 website/app/logs/page.tsx diff --git a/website/app/components/dashboard/ActivityList.tsx b/website/app/components/dashboard/ActivityList.tsx index 0052ae8e..5126737c 100644 --- a/website/app/components/dashboard/ActivityList.tsx +++ b/website/app/components/dashboard/ActivityList.tsx @@ -1,3 +1,5 @@ +import Link from "next/link"; + export interface Activity { id: string; title: string; @@ -8,6 +10,7 @@ export interface Activity { interface ActivityListProps { activities: Activity[]; + getActivityHref?: (activity: Activity) => string; } const CATEGORY_STYLES: Record = { @@ -16,27 +19,32 @@ const CATEGORY_STYLES: Record = { Relationships: "bg-amber-500/15 text-amber-300", }; -export default function ActivityList({ activities }: ActivityListProps) { +export default function ActivityList({ activities, getActivityHref }: ActivityListProps) { return (
{activities.map((activity) => ( -
- {activity.icon} -
-

{activity.title}

-

{activity.time}

-
- - {activity.category} - -
+ {activity.icon} +
+

{activity.title}

+

{activity.time}

+
+ + {activity.category} + +
+ ))}
); diff --git a/website/app/components/dashboard/DonutChart.tsx b/website/app/components/dashboard/DonutChart.tsx index 34e8b63a..b6f63ef5 100644 --- a/website/app/components/dashboard/DonutChart.tsx +++ b/website/app/components/dashboard/DonutChart.tsx @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import { useRouter } from "next/navigation"; export interface CategoryData { label: string; @@ -12,12 +13,14 @@ interface DonutChartProps { data: CategoryData[]; size?: number; strokeWidth?: number; + getCategoryHref?: (label: string) => string; } const SEGMENT_GAP = 4; -export default function DonutChart({ data, size = 216, strokeWidth = 20 }: DonutChartProps) { +export default function DonutChart({ data, size = 216, strokeWidth = 20, getCategoryHref }: DonutChartProps) { const [hovered, setHovered] = useState(null); + const router = useRouter(); const center = size / 2; const radius = (size - strokeWidth) / 2; @@ -37,6 +40,10 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut const activeSegment = hovered ? segments.find((s) => s.label === hovered) : null; + const handleCategoryClick = (label: string) => { + router.push(getCategoryHref ? getCategoryHref(label) : `/logs?category=${encodeURIComponent(label.toLowerCase())}`); + }; + return (
@@ -72,6 +79,7 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut }} onMouseEnter={() => setHovered(seg.label)} onMouseLeave={() => setHovered(null)} + onClick={() => handleCategoryClick(seg.label)} /> ))} @@ -105,6 +113,7 @@ export default function DonutChart({ data, size = 216, strokeWidth = 20 }: Donut className="cursor-pointer group" onMouseEnter={() => setHovered(seg.label)} onMouseLeave={() => setHovered(null)} + onClick={() => handleCategoryClick(seg.label)} >
diff --git a/website/app/components/dashboard/TodayOverview.tsx b/website/app/components/dashboard/TodayOverview.tsx index 1d937d8e..a72bbba4 100644 --- a/website/app/components/dashboard/TodayOverview.tsx +++ b/website/app/components/dashboard/TodayOverview.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import DonutChart, { CategoryData } from "./DonutChart"; import ActivityList, { Activity } from "./ActivityList"; @@ -19,6 +20,8 @@ const MOCK_ACTIVITIES: Activity[] = [ ]; export default function TodayOverview() { + const selectedDate = "2026-02-23"; + const today = new Date().toLocaleDateString("en-US", { weekday: "long", month: "long", @@ -33,9 +36,17 @@ export default function TodayOverview() {

Today's Overview

{today}

- - Today - +
+ + Today + + + View All Logs -> + +
{/* Card body */} @@ -43,13 +54,23 @@ export default function TodayOverview() { {/* Left: Donut chart */}

Time Distribution

- + `/logs?category=${encodeURIComponent(label.toLowerCase())}&from=dashboard`} + />
{/* Right: Activity list */}

Today's Activities

- + + `/logs?date=${selectedDate}&highlight=${encodeURIComponent(activity.title)}&category=${encodeURIComponent( + activity.category.toLowerCase() + )}&from=dashboard` + } + />
diff --git a/website/app/components/logs/ClassificationPanel.tsx b/website/app/components/logs/ClassificationPanel.tsx new file mode 100644 index 00000000..5d19adb6 --- /dev/null +++ b/website/app/components/logs/ClassificationPanel.tsx @@ -0,0 +1,47 @@ +import { CATEGORY_STYLES, MOCK_CLASSIFICATION_EXAMPLES } from "./mockData"; + +export default function ClassificationPanel() { + return ( +
+
+

Data Classification

+

How AI parses raw messages into meaningful categories.

+
+ + {MOCK_CLASSIFICATION_EXAMPLES.map((example) => ( +
+ +

“{example.rawMessage}”

+ Expand +
+ +
+
+

Raw Message

+

{example.rawMessage}

+
+ +
+

Extracted Categories

+
+ {example.extractedCategories.map((category) => ( + + {category} + + ))} +
+
+ +
+

Classification Notes

+

{example.notes}

+
+
+
+ ))} +
+ ); +} diff --git a/website/app/components/logs/LogItem.tsx b/website/app/components/logs/LogItem.tsx new file mode 100644 index 00000000..baeb84a3 --- /dev/null +++ b/website/app/components/logs/LogItem.tsx @@ -0,0 +1,38 @@ +import { CATEGORY_STYLES, LOG_TYPE_STYLES, LogEntry } from "./mockData"; + +interface LogItemProps { + log: LogEntry; + isHighlighted?: boolean; +} + +export default function LogItem({ log, isHighlighted = false }: LogItemProps) { + return ( +
+
+
+

{log.time}

+
+ +
+

{log.text}

+
+ {log.categories.map((category) => ( + + {category} + + ))} + + {log.type} + +
+
+
+
+ ); +} diff --git a/website/app/components/logs/LogsControlBar.tsx b/website/app/components/logs/LogsControlBar.tsx new file mode 100644 index 00000000..0338457d --- /dev/null +++ b/website/app/components/logs/LogsControlBar.tsx @@ -0,0 +1,83 @@ +import { DAILY_OPTIONS, DailyOption, LogsView, RECENT_COUNT_OPTIONS, RecentCountOption } from "./mockData"; + +interface LogsControlBarProps { + view: LogsView; + selectedDay: DailyOption; + selectedRecentCount: RecentCountOption; + onViewChange: (view: LogsView) => void; + onDayChange: (day: DailyOption) => void; + onRecentCountChange: (count: RecentCountOption) => void; +} + +function ToggleButton({ + active, + label, + onClick, +}: { + active: boolean; + label: string; + onClick: () => void; +}) { + return ( + + ); +} + +export default function LogsControlBar({ + view, + selectedDay, + selectedRecentCount, + onViewChange, + onDayChange, + onRecentCountChange, +}: LogsControlBarProps) { + return ( +
+
+ onViewChange("daily")} /> + onViewChange("recent")} /> +
+ + {view === "daily" ? ( + + ) : ( + + )} +
+ ); +} diff --git a/website/app/components/logs/LogsList.tsx b/website/app/components/logs/LogsList.tsx new file mode 100644 index 00000000..8d84a2d7 --- /dev/null +++ b/website/app/components/logs/LogsList.tsx @@ -0,0 +1,41 @@ +import LogItem from "./LogItem"; +import { LogEntry } from "./mockData"; + +interface LogsListProps { + logs: LogEntry[]; + highlightedLogText?: string | null; +} + +export default function LogsList({ logs, highlightedLogText }: LogsListProps) { + return ( +
+
+

Logs Explorer

+

{logs.length} messages

+
+ +
+ {logs.map((log) => ( + + ))} +
+ +
+ + Page 1 + +
+
+ ); +} diff --git a/website/app/components/logs/LogsPage.tsx b/website/app/components/logs/LogsPage.tsx new file mode 100644 index 00000000..b59e3464 --- /dev/null +++ b/website/app/components/logs/LogsPage.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useMemo, useState } from "react"; +import Link from "next/link"; +import { useSearchParams } from "next/navigation"; +import ClassificationPanel from "./ClassificationPanel"; +import LogsControlBar from "./LogsControlBar"; +import LogsList from "./LogsList"; +import { DailyOption, LogsView, MOCK_LOGS, RecentCountOption } from "./mockData"; + +function formatCategoryLabel(value: string) { + if (!value) return ""; + return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase(); +} + +export default function LogsPage() { + const searchParams = useSearchParams(); + const [view, setView] = useState("daily"); + const [selectedDay, setSelectedDay] = useState("Feb 23"); + const [selectedRecentCount, setSelectedRecentCount] = useState(10); + const source = searchParams.get("from"); + const selectedDate = searchParams.get("date"); + const selectedCategory = searchParams.get("category"); + const highlightedText = searchParams.get("highlight"); + + const visibleLogs = useMemo(() => { + if (view === "recent") { + return MOCK_LOGS.slice(0, selectedRecentCount); + } + return MOCK_LOGS; + }, [view, selectedRecentCount, selectedDay]); + + const contextBannerText = useMemo(() => { + if (selectedDate) { + return `Showing logs for: ${selectedDate}`; + } + if (selectedCategory) { + return `Filtered by: ${formatCategoryLabel(selectedCategory)}`; + } + return null; + }, [selectedDate, selectedCategory]); + + return ( +
+
+
+
+ + Dashboard + + / + Logs +
+ + ← Back to Dashboard + +
+ +
+

Activity Logs

+

Explore how messages are classified into insights

+
+ + {contextBannerText && ( +
+ {contextBannerText} + {source === "dashboard" ? " (from Dashboard)" : ""} +
+ )} + + + +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/website/app/components/logs/mockData.ts b/website/app/components/logs/mockData.ts new file mode 100644 index 00000000..0e6cf6a3 --- /dev/null +++ b/website/app/components/logs/mockData.ts @@ -0,0 +1,101 @@ +export type LogCategory = "Work" | "Health" | "Relationships" | "Other"; +export type LogType = "Life Log" | "Ignored"; +export type LogsView = "daily" | "recent"; +export type DailyOption = "Feb 23" | "Feb 22" | "Feb 21"; +export type RecentCountOption = 10 | 20 | 50; + +export interface LogEntry { + id: string; + time: string; + text: string; + categories: LogCategory[]; + type: LogType; +} + +export interface ClassificationExample { + id: string; + rawMessage: string; + extractedCategories: LogCategory[]; + notes: string; +} + +export const CATEGORY_STYLES: Record = { + Work: "bg-blue-500/15 text-blue-300 border border-blue-500/25", + Health: "bg-emerald-500/15 text-emerald-300 border border-emerald-500/25", + Relationships: "bg-amber-500/15 text-amber-300 border border-amber-500/25", + Other: "bg-slate-500/20 text-slate-300 border border-slate-500/25", +}; + +export const LOG_TYPE_STYLES: Record = { + "Life Log": "bg-violet-500/15 text-violet-300 border border-violet-500/25", + Ignored: "bg-rose-500/15 text-rose-300 border border-rose-500/25", +}; + +export const DAILY_OPTIONS: DailyOption[] = ["Feb 23", "Feb 22", "Feb 21"]; +export const RECENT_COUNT_OPTIONS: RecentCountOption[] = [10, 20, 50]; + +export const MOCK_LOGS: LogEntry[] = [ + { + id: "log-1", + time: "06:45", + text: "Morning workout", + categories: ["Health"], + type: "Life Log", + }, + { + id: "log-2", + time: "08:30", + text: "Focused sprint for quarterly planning", + categories: ["Work"], + type: "Life Log", + }, + { + id: "log-3", + time: "09:05", + text: "What's the weather today?", + categories: ["Other"], + type: "Ignored", + }, + { + id: "log-4", + time: "12:20", + text: "Lunch with my sister", + categories: ["Relationships", "Health"], + type: "Life Log", + }, + { + id: "log-5", + time: "15:10", + text: "Reminder to pay electricity bill", + categories: ["Other"], + type: "Ignored", + }, + { + id: "log-6", + time: "19:40", + text: "Evening walk and podcast", + categories: ["Health"], + type: "Life Log", + }, +]; + +export const MOCK_CLASSIFICATION_EXAMPLES: ClassificationExample[] = [ + { + id: "example-1", + rawMessage: "Had standup, then did deep work, and called mom in the evening", + extractedCategories: ["Work", "Relationships"], + notes: "Multiple high-signal actions were grouped as meaningful life activity.", + }, + { + id: "example-2", + rawMessage: "Can you check if it will rain tomorrow?", + extractedCategories: ["Other"], + notes: "Utility query with low journaling value, marked as non-log.", + }, + { + id: "example-3", + rawMessage: "Gym session before breakfast and a 20-minute walk after dinner", + extractedCategories: ["Health"], + notes: "Repeated behavior with clear wellbeing relevance, stored as life log.", + }, +]; diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx index 9da17312..e15e943f 100644 --- a/website/app/dashboard/page.tsx +++ b/website/app/dashboard/page.tsx @@ -6,7 +6,6 @@ import Link from "next/link"; import TodayOverview from "@/app/components/dashboard/TodayOverview"; import HabitHeatmap from "@/app/components/dashboard/HabitHeatmap"; import GoalsSection from "@/app/components/dashboard/GoalsSection"; -import ActivityLogsSection from "@/app/components/dashboard/ActivityLogsSection"; import { useState, useRef, useEffect, useCallback } from "react"; interface WhatsAppSession { @@ -660,8 +659,19 @@ export default function DashboardPage() { {/* Goals & Progress */} - {/* Activity Logs & Classification */} - + {/* Activity Logs entry point */} +
+
+

Activity Logs

+

Jump from your overview into detailed logs exploration

+
+ + View All Logs -> + +
{/* Main Content */}
diff --git a/website/app/logs/page.tsx b/website/app/logs/page.tsx new file mode 100644 index 00000000..b1b80944 --- /dev/null +++ b/website/app/logs/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import LogsPage from "@/app/components/logs/LogsPage"; + +export default function LogsRoutePage() { + return ; +} From ba968f0ca7083ba403e2daa2380f738502ef0263 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 14:43:36 +0500 Subject: [PATCH 11/78] Polish nav UI: better arrow on View All Logs, remove redundant Back to Dashboard link --- website/app/components/dashboard/TodayOverview.tsx | 8 ++++++-- website/app/components/logs/LogsPage.tsx | 5 +---- website/app/dashboard/page.tsx | 8 ++++++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/website/app/components/dashboard/TodayOverview.tsx b/website/app/components/dashboard/TodayOverview.tsx index a72bbba4..e1ab6356 100644 --- a/website/app/components/dashboard/TodayOverview.tsx +++ b/website/app/components/dashboard/TodayOverview.tsx @@ -42,9 +42,13 @@ export default function TodayOverview() { - View All Logs -> + View All Logs + + + +
diff --git a/website/app/components/logs/LogsPage.tsx b/website/app/components/logs/LogsPage.tsx index b59e3464..6ed9f773 100644 --- a/website/app/components/logs/LogsPage.tsx +++ b/website/app/components/logs/LogsPage.tsx @@ -43,7 +43,7 @@ export default function LogsPage() { return (
-
+
Dashboard @@ -51,9 +51,6 @@ export default function LogsPage() { / Logs
- - ← Back to Dashboard -
diff --git a/website/app/dashboard/page.tsx b/website/app/dashboard/page.tsx index e15e943f..40f227e4 100644 --- a/website/app/dashboard/page.tsx +++ b/website/app/dashboard/page.tsx @@ -667,9 +667,13 @@ export default function DashboardPage() {
- View All Logs -> + View All Logs + + + +
From b14b1a0286ba58770730a6b3cf4dc01caee4f610 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 14:55:24 +0500 Subject: [PATCH 12/78] Comment out Goals sort toggle and Needs attention summary --- website/app/components/dashboard/GoalsSection.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/website/app/components/dashboard/GoalsSection.tsx b/website/app/components/dashboard/GoalsSection.tsx index 0b2ec39e..d8939879 100644 --- a/website/app/components/dashboard/GoalsSection.tsx +++ b/website/app/components/dashboard/GoalsSection.tsx @@ -73,8 +73,8 @@ const MOCK_GOALS: Goal[] = [ type SortMode = "streak" | "needs-work"; const SORT_OPTIONS: { id: SortMode; label: string }[] = [ - { id: "streak", label: "🔥 Top Streaks" }, - { id: "needs-work", label: "⚠️ Needs Work" }, + // { id: "streak", label: "🔥 Top Streaks" }, + // { id: "needs-work", label: "⚠️ Needs Work" }, ]; function sortGoals(goals: Goal[], mode: SortMode): Goal[] { @@ -114,7 +114,7 @@ export default function GoalsSection() {

{MOCK_GOALS.length} active goals this month

- {/* Sort toggle */} + {/* Sort toggle – commented out for now
{SORT_OPTIONS.map((opt) => (
+ */}
{/* Goal cards grid */} @@ -163,6 +164,7 @@ export default function GoalsSection() { days

+ {/* Needs attention – commented out for now {stats.struggling > 0 && (

Needs attention

@@ -172,6 +174,7 @@ export default function GoalsSection() {

)} + */}

last 7 days shown per goal

From 122e6860ec1a5fa85b3306985b6b63f0b3b9f0ad Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 15:15:01 +0500 Subject: [PATCH 13/78] Remove Duration label from AI Extraction concept tags --- website/app/components/dashboard/AIProcessingCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/website/app/components/dashboard/AIProcessingCard.tsx b/website/app/components/dashboard/AIProcessingCard.tsx index 5a27f0d4..31cfd2c1 100644 --- a/website/app/components/dashboard/AIProcessingCard.tsx +++ b/website/app/components/dashboard/AIProcessingCard.tsx @@ -15,7 +15,6 @@ const CONCEPT_TAGS = [ { label: "Health", style: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20" }, { label: "Work", style: "bg-blue-500/10 text-blue-400 border-blue-500/20" }, { label: "Relationships", style: "bg-amber-500/10 text-amber-400 border-amber-500/20" }, - { label: "Duration", style: "bg-violet-500/10 text-violet-400 border-violet-500/20" }, ]; // ─── AIProcessingCard ───────────────────────────────────────────────────────── From c9740dc725ce0fab52a2aaf9b03b646118dc2d5d Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Tue, 24 Feb 2026 19:37:35 +0500 Subject: [PATCH 14/78] fix: resolve eslint errors and warnings in dashboard and logs components --- website/app/components/dashboard/AIPipeline.tsx | 10 +++++----- .../app/components/dashboard/AnimatedProgressBar.tsx | 2 +- website/app/components/dashboard/GoalsSection.tsx | 7 +------ website/app/components/logs/LogsPage.tsx | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/website/app/components/dashboard/AIPipeline.tsx b/website/app/components/dashboard/AIPipeline.tsx index d5f38c57..438b2fdc 100644 --- a/website/app/components/dashboard/AIPipeline.tsx +++ b/website/app/components/dashboard/AIPipeline.tsx @@ -65,16 +65,16 @@ export default function AIPipeline({ const [activeMappingStep, setActiveMappingStep] = useState(0); useEffect(() => { - if (!isActive) { - setActiveMappingStep(0); - return; - } + if (!isActive) return; const id = setInterval(() => { setActiveMappingStep((prev) => (prev + 1) % 4); }, 1800); - return () => clearInterval(id); + return () => { + clearInterval(id); + setActiveMappingStep(0); + }; }, [isActive]); return ( diff --git a/website/app/components/dashboard/AnimatedProgressBar.tsx b/website/app/components/dashboard/AnimatedProgressBar.tsx index 600c4fda..9b42d336 100644 --- a/website/app/components/dashboard/AnimatedProgressBar.tsx +++ b/website/app/components/dashboard/AnimatedProgressBar.tsx @@ -21,7 +21,7 @@ export default function AnimatedProgressBar({ activeMappingStep }: AnimatedProgr />
-

"5h deep work" mapped to today's work target

+

“5h deep work” mapped to today's work target

); } diff --git a/website/app/components/dashboard/GoalsSection.tsx b/website/app/components/dashboard/GoalsSection.tsx index d8939879..58e0bedc 100644 --- a/website/app/components/dashboard/GoalsSection.tsx +++ b/website/app/components/dashboard/GoalsSection.tsx @@ -72,11 +72,6 @@ const MOCK_GOALS: Goal[] = [ type SortMode = "streak" | "needs-work"; -const SORT_OPTIONS: { id: SortMode; label: string }[] = [ - // { id: "streak", label: "🔥 Top Streaks" }, - // { id: "needs-work", label: "⚠️ Needs Work" }, -]; - function sortGoals(goals: Goal[], mode: SortMode): Goal[] { return [...goals].sort((a, b) => mode === "streak" @@ -100,7 +95,7 @@ function computeStats(goals: Goal[]) { // ─── GoalsSection ───────────────────────────────────────────────────────────── export default function GoalsSection() { - const [sortMode, setSortMode] = useState("streak"); + const [sortMode] = useState("streak"); const sorted = useMemo(() => sortGoals(MOCK_GOALS, sortMode), [sortMode]); const stats = useMemo(() => computeStats(MOCK_GOALS), []); diff --git a/website/app/components/logs/LogsPage.tsx b/website/app/components/logs/LogsPage.tsx index 6ed9f773..74437a14 100644 --- a/website/app/components/logs/LogsPage.tsx +++ b/website/app/components/logs/LogsPage.tsx @@ -28,7 +28,7 @@ export default function LogsPage() { return MOCK_LOGS.slice(0, selectedRecentCount); } return MOCK_LOGS; - }, [view, selectedRecentCount, selectedDay]); + }, [view, selectedRecentCount]); const contextBannerText = useMemo(() => { if (selectedDate) { From 8b7ecc34a0ccea3409cca7cdd0767788e8528877 Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Wed, 25 Feb 2026 08:26:27 +0500 Subject: [PATCH 15/78] Wrap logs page in Suspense for useSearchParams --- website/app/logs/page.tsx | 7 ++++++- website/next-env.d.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/website/app/logs/page.tsx b/website/app/logs/page.tsx index b1b80944..f16286c1 100644 --- a/website/app/logs/page.tsx +++ b/website/app/logs/page.tsx @@ -1,7 +1,12 @@ "use client"; +import { Suspense } from "react"; import LogsPage from "@/app/components/logs/LogsPage"; export default function LogsRoutePage() { - return ; + return ( + Loading...
}> + + + ); } diff --git a/website/next-env.d.ts b/website/next-env.d.ts index c4b7818f..9edff1c7 100644 --- a/website/next-env.d.ts +++ b/website/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. From ad3aad61edf5f549883f091371cdfad00e94d7ed Mon Sep 17 00:00:00 2001 From: hafiz-ahtasham-ali Date: Wed, 25 Feb 2026 08:56:52 +0500 Subject: [PATCH 16/78] Layout: make Month Snapshot card fill horizontal space and match calendar height --- website/app/components/dashboard/HabitHeatmap.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/app/components/dashboard/HabitHeatmap.tsx b/website/app/components/dashboard/HabitHeatmap.tsx index 2d092178..2913a923 100644 --- a/website/app/components/dashboard/HabitHeatmap.tsx +++ b/website/app/components/dashboard/HabitHeatmap.tsx @@ -101,8 +101,8 @@ function StatsPanel({ currentStreak, }: StatsPanelProps) { return ( -