UNPKG

oneie

Version:

Build apps, websites, and AI agents in English. Zero-interaction setup for AI agents (Claude Code, Cursor, Windsurf). Download to your computer, run in the cloud, deploy to the edge. Open source and free forever.

1,629 lines (1,326 loc) 61.7 kB
--- title: 2 6 Dashboard Migration dimension: things category: features tags: ai, backend, frontend related_dimensions: events, knowledge, people scope: global created: 2025-11-03 updated: 2025-11-03 version: 1.0.0 ai_context: | This document is part of the things dimension in the features category. Location: one/things/features/2-6-dashboard-migration.md Purpose: Documents feature 2-6: dashboard component migration Related dimensions: events, knowledge, people For AI agents: Read this to understand 2 6 dashboard migration. --- # Feature 2-6: Dashboard Component Migration **Feature ID:** `feature_2_6_dashboard_migration` **Plan:** `plan_2_backend_agnostic_frontend` **Owner:** Frontend Specialist **Status:** Ready for Implementation **Priority:** P1 (High - Complete Migration) **Effort:** 1 week (7 days) **Dependencies:** Feature 2-5 (Auth Migration Complete) --- ## 1. Complete Technical Specification ### Overview Migrate all dashboard components from direct Convex integration to the backend-agnostic DataProvider pattern. This migration enables the dashboard to work with any backend (Convex, Supabase, Firebase) while maintaining all existing functionality, performance, and user experience. ### Goals 1. **Backend Agnosticism**: Remove all direct Convex imports from dashboard components 2. **Functional Parity**: Preserve all existing dashboard features and behavior 3. **Performance Maintenance**: Ensure no performance degradation (within 10% baseline) 4. **Real-time Updates**: Maintain real-time data subscriptions and updates 5. **Type Safety**: Maintain full TypeScript type safety through DataProvider 6. **Test Coverage**: Update all tests to use DataProvider mocks ### Dashboard Architecture ``` Dashboard Component Tree: ├── DashboardLayout.tsx # Main layout wrapper │ ├── Sidebar.tsx # Navigation (org-scoped) │ ├── Header.tsx # User profile, notifications │ └── Content Area │ ├── DashboardOverview.tsx # Main dashboard (multiple hooks) │ │ ├── StatsCards.tsx # Analytics (aggregated data) │ │ ├── ActivityCharts.tsx # Charts (time-series data) │ │ ├── RevenueCharts.tsx # Revenue analytics │ │ └── RecentTransactions # Transaction list │ ├── CourseList.tsx # Entity list component │ ├── CourseDetail.tsx # Entity detail component │ ├── LessonList.tsx # Entity list component │ ├── LessonDetail.tsx # Entity detail component │ ├── StudentList.tsx # Entity list component │ ├── ActivityFeed.tsx # Event stream component │ ├── SearchBar.tsx # Search component │ └── SettingsPanel.tsx # Settings UI ``` ### Component Categories and Hook Mapping #### 1. Entity List Components (useThings) **Components:** - `CourseList.tsx` - Display all courses for org - `LessonList.tsx` - Display lessons for course - `StudentList.tsx` - Display enrolled students - `AICloneList.tsx` - Display AI agents **Hook Migration:** ```typescript // BEFORE: Direct Convex const courses = useQuery(api.queries.entities.list, { type: "course" }); // AFTER: DataProvider const { data: courses, loading, error } = useThings({ type: "course" }); ``` **Features to Preserve:** - Filtering by status, organization - Sorting by createdAt, updatedAt, name - Pagination support - Real-time updates - Loading states - Error handling #### 2. Entity Detail Components (useThing, useConnections) **Components:** - `CourseDetail.tsx` - Display course details + relationships - `LessonDetail.tsx` - Display lesson details - `StudentProfile.tsx` - Display student profile **Hook Migration:** ```typescript // BEFORE: Direct Convex const course = useQuery(api.queries.entities.get, { id: courseId }); const students = useQuery(api.queries.connections.getRelated, { fromThingId: courseId, relationshipType: "enrolled_in", }); // AFTER: DataProvider const { data: course, loading, error } = useThing({ id: courseId }); const { data: students, loading: studentsLoading } = useConnections({ fromThingId: courseId, relationshipType: "enrolled_in", }); ``` **Features to Preserve:** - Display entity properties - Display related entities (connections) - Edit entity (useUpdateThing) - Delete entity (useDeleteThing) - Real-time updates - Optimistic updates #### 3. Activity Feed Component (useEvents) **Component:** - `ActivityFeed.tsx` - Display audit trail **Hook Migration:** ```typescript // BEFORE: Direct Convex const events = useQuery(api.queries.events.getRecent, { actorId: userId, limit: 20, }); // AFTER: DataProvider const { data: events, loading } = useEvents({ actorId: userId, limit: 20, }); ``` **Features to Preserve:** - Filter by actor, target, event type - Real-time updates - Pagination (load more) - Event formatting - Timestamps #### 4. Search Components (useSearch) **Component:** - `SearchBar.tsx` - Full-text search with autocomplete **Hook Migration:** ```typescript // BEFORE: Direct Convex const results = useQuery(api.queries.search.fullText, { query: searchQuery, types: ["course", "lesson"], }); // AFTER: DataProvider const { data: results, loading } = useSearch({ query: searchQuery, types: ["course", "lesson"], }); ``` **Features to Preserve:** - Type-ahead suggestions - Filter by entity type - Debounced search - RAG integration - Highlighting #### 5. Analytics Components (Multiple Hooks) **Component:** - `DashboardOverview.tsx` - Main dashboard with stats - `StatsCards.tsx` - Aggregate metrics - `ActivityCharts.tsx` - Time-series charts - `RevenueCharts.tsx` - Revenue analytics **Hook Migration:** ```typescript // BEFORE: Direct Convex const courseCount = useQuery(api.queries.analytics.countByType, { type: "course", }); const studentCount = useQuery(api.queries.analytics.countByType, { type: "student", }); const recentActivity = useQuery(api.queries.events.getRecent, { limit: 10 }); // AFTER: DataProvider const { data: courses } = useThings({ type: "course" }); const { data: students } = useThings({ type: "student" }); const { data: recentActivity } = useEvents({ limit: 10 }); // Client-side aggregation const courseCount = courses?.length || 0; const studentCount = students?.length || 0; ``` **Features to Preserve:** - Aggregate counts - Time-series data - Real-time updates - Chart rendering - Loading skeletons --- ## 2. Ontology Mapping (All 6 Dimensions) ### Organizations Dimension **Dashboard Isolation:** - All dashboard data is org-scoped - Queries automatically filter by `organizationId` - Sidebar shows current organization name/logo - Org switcher for users with multiple orgs - Org-level analytics and metrics **Implementation:** ```typescript // DataProvider automatically adds organizationId to all queries const { data: courses } = useThings({ type: "course", // organizationId: currentOrgId added automatically by DataProvider }); ``` **Org-Scoped Components:** - `DashboardOverview` - Shows org metrics - `CourseList` - Shows org courses - `StudentList` - Shows org students - `ActivityFeed` - Shows org events - `SettingsPanel` - Shows org settings ### People Dimension **Role-Based Dashboard:** - `platform_owner` - See all organizations, platform-wide metrics - `org_owner` - See org dashboard, admin controls - `org_user` - See limited dashboard, own courses - `customer` - See marketplace, enrolled courses **Permission-Based UI:** ```typescript function DashboardOverview() { const { user, role } = useAuth(); return ( <div> {/* All roles see their own stats */} <StatsCards /> {/* Org owners see admin controls */} {(role === "org_owner" || role === "platform_owner") && ( <AdminPanel /> )} {/* Platform owners see platform metrics */} {role === "platform_owner" && ( <PlatformMetrics /> )} </div> ); } ``` **People-Scoped Components:** - `ActivityFeed` - Filter by actorId (current user) - `CourseList` - Show courses owned by user - `StudentList` - Show students enrolled in user's courses ### Things Dimension **Entity Types Displayed:** - `course` - Online courses - `lesson` - Course lessons - `ai_clone` - AI agents - `creator` - Course creators/instructors - `student` - Enrolled students - `blog_post` - Content - `token` - Digital assets - `organization` - Organizations **Entity Status Badges:** ```typescript function EntityStatusBadge({ status }: { status: ThingStatus }) { const statusStyles = { draft: "bg-gray-100 text-gray-800", active: "bg-green-100 text-green-800", published: "bg-blue-100 text-blue-800", archived: "bg-red-100 text-red-800" }; return ( <span className={`px-2 py-1 rounded ${statusStyles[status]}`}> {status} </span> ); } ``` **Thing-Scoped Components:** - `CourseList` - Display course entities - `CourseDetail` - Display single course entity - `LessonList` - Display lesson entities - `StudentList` - Display student entities ### Connections Dimension **Relationships Displayed:** - `owns` - Creator owns course - `enrolled_in` - Student enrolled in course - `authored` - Creator authored content - `following` - User following creator - `holds_tokens` - User holds tokens **Connection Display:** ```typescript function CourseStudents({ courseId }: { courseId: Id<"things"> }) { const { data: enrollments, loading } = useConnections({ toThingId: courseId, relationshipType: "enrolled_in" }); if (loading) return <LoadingSkeleton />; return ( <div> <h3>Enrolled Students ({enrollments?.length})</h3> {enrollments?.map(enrollment => ( <StudentCard key={enrollment._id} studentId={enrollment.fromThingId} enrolledAt={enrollment.createdAt} /> ))} </div> ); } ``` **Connection-Scoped Components:** - `CourseDetail` - Show enrolled students (connections) - `StudentProfile` - Show enrolled courses (connections) - `ActivityFeed` - Show connection events (follow, enroll) ### Events Dimension **Event Types Displayed:** - `course_created` - Course created - `course_updated` - Course updated - `student_enrolled` - Student enrolled - `lesson_completed` - Lesson completed - `content_published` - Content published - `token_transferred` - Token transferred **Event Stream:** ```typescript function ActivityFeed({ actorId }: { actorId?: Id<"things"> }) { const { data: events, loading } = useEvents({ actorId, limit: 20 }); if (loading) return <LoadingSkeleton />; return ( <div className="space-y-4"> {events?.map(event => ( <EventCard key={event._id} event={event} showActor={!actorId} showTimestamp /> ))} </div> ); } ``` **Event-Scoped Components:** - `ActivityFeed` - Display event stream - `DashboardOverview` - Show recent events - `CourseDetail` - Show course-specific events ### Knowledge Dimension **Search and Discovery:** - Full-text search across entities - RAG-powered content suggestions - Knowledge labels (tags) for categorization - Vector search for semantic similarity **Search Implementation:** ```typescript function SearchBar() { const [query, setQuery] = useState(""); const { data: results, loading } = useSearch({ query, types: ["course", "lesson", "blog_post"] }); return ( <div> <input type="search" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search courses, lessons, content..." /> {loading && <LoadingSpinner />} {results?.map(result => ( <SearchResult key={result._id} result={result} /> ))} </div> ); } ``` **Knowledge-Scoped Components:** - `SearchBar` - Full-text search - `ContentRecommendations` - RAG suggestions - `TagFilter` - Filter by knowledge labels --- ## 3. User Stories (10 Stories with Acceptance Criteria) ### Story 1: Creator Views Course Dashboard **As a** course creator **I want to** see my courses in the dashboard using DataProvider **So that** the dashboard works with any backend and I can manage my courses **Acceptance Criteria:** - [ ] CourseList component uses `useThings` hook - [ ] Loading state displays skeleton - [ ] Courses display with name, thumbnail, status, student count - [ ] Courses are org-scoped (only current org) - [ ] Real-time updates when course is created/updated - [ ] Error state displays user-friendly message - [ ] No direct Convex imports in component ### Story 2: Org Owner Views Analytics **As an** organization owner **I want to** see aggregated analytics in the dashboard **So that** I can track org performance and growth **Acceptance Criteria:** - [ ] StatsCards component uses multiple DataProvider hooks - [ ] Displays course count, student count, revenue, engagement - [ ] Loading states for each metric - [ ] Real-time updates when data changes - [ ] Charts render with proper data aggregation - [ ] No direct Convex imports in component ### Story 3: Instructor Views Enrolled Students **As an** instructor **I want to** see students enrolled in my course **So that** I can track enrollment and student progress **Acceptance Criteria:** - [ ] CourseDetail uses `useThing` and `useConnections` hooks - [ ] Displays course details and enrolled students - [ ] Student list shows name, email, enrollment date - [ ] Loading states for course and students - [ ] Real-time updates when student enrolls - [ ] No direct Convex imports in component ### Story 4: User Views Activity Feed **As a** user **I want to** see recent activity in my dashboard **So that** I can stay updated on platform events **Acceptance Criteria:** - [ ] ActivityFeed component uses `useEvents` hook - [ ] Displays recent events with actor, action, timestamp - [ ] Loading state displays skeleton - [ ] Real-time updates when events occur - [ ] Pagination support (load more) - [ ] Filter by event type - [ ] No direct Convex imports in component ### Story 5: Creator Searches Content **As a** creator **I want to** search for courses, lessons, and content **So that** I can quickly find what I need **Acceptance Criteria:** - [ ] SearchBar component uses `useSearch` hook - [ ] Type-ahead suggestions as I type - [ ] Filter by entity type (course, lesson, blog_post) - [ ] Debounced search (300ms delay) - [ ] Loading state during search - [ ] Highlighting of search terms - [ ] No direct Convex imports in component ### Story 6: Instructor Edits Course **As an** instructor **I want to** edit my course from the dashboard **So that** I can update course details **Acceptance Criteria:** - [ ] CourseDetail uses `useUpdateThing` hook - [ ] Displays course edit form - [ ] Optimistic update (UI updates immediately) - [ ] Success/error toast notifications - [ ] Validation errors display inline - [ ] Real-time updates reflect in list view - [ ] No direct Convex imports in component ### Story 7: Student Views Enrolled Courses **As a** student **I want to** see my enrolled courses in the dashboard **So that** I can access my learning materials **Acceptance Criteria:** - [ ] StudentDashboard uses `useConnections` hook - [ ] Displays courses student is enrolled in - [ ] Shows course progress and completion - [ ] Loading state displays skeleton - [ ] Real-time updates when enrolled in new course - [ ] No direct Convex imports in component ### Story 8: Org Owner Views Revenue Charts **As an** organization owner **I want to** see revenue charts in the dashboard **So that** I can track financial performance **Acceptance Criteria:** - [ ] RevenueCharts uses `useEvents` hook for transaction events - [ ] Aggregates revenue by day/week/month - [ ] Displays line chart and bar chart - [ ] Loading state displays skeleton - [ ] Real-time updates when transactions occur - [ ] Export chart data to CSV - [ ] No direct Convex imports in component ### Story 9: Platform Owner Views All Organizations **As a** platform owner **I want to** see all organizations in the dashboard **So that** I can manage the platform **Acceptance Criteria:** - [ ] PlatformDashboard uses `useOrganizations` hook - [ ] Displays all organizations with stats - [ ] Shows org name, plan, user count, usage - [ ] Loading state displays skeleton - [ ] Real-time updates when org is created/updated - [ ] Filter by plan (starter, pro, enterprise) - [ ] No direct Convex imports in component ### Story 10: User Views Dashboard on Mobile **As a** mobile user **I want to** view the dashboard on my phone **So that** I can access my data anywhere **Acceptance Criteria:** - [ ] DashboardLayout is fully responsive - [ ] Mobile menu works correctly - [ ] Charts render properly on small screens - [ ] Touch interactions work (swipe, tap) - [ ] Loading states display on mobile - [ ] Performance is acceptable (< 3s load time) - [ ] No direct Convex imports in component --- ## 4. Implementation Steps (50 Steps) ### Phase 1: Audit and Preparation (Day 1) **Step 1-10: Audit Current Implementation** 1. **Audit all dashboard components** - List all files in `frontend/src/components/dashboard/` 2. **Identify Convex imports** - Grep for `import { useQuery, useMutation } from "convex/react"` 3. **Map components to hooks** - Create mapping table (component → hooks needed) 4. **Document current functionality** - Screenshot all dashboard pages, document features 5. **Review tests** - Identify all test files in `frontend/test/components/dashboard/` 6. **Create migration checklist** - Component-by-component checklist 7. **Set up feature branch** - `git checkout -b feature/dashboard-migration` 8. **Create rollback points** - Tag commits for easy rollback 9. **Review DataProvider hooks** - Ensure all needed hooks exist 10. **Review performance baseline** - Run Lighthouse, document metrics **Step 11-15: Prepare Test Infrastructure** 11. **Create DataProvider mocks** - Mock implementations for tests 12. **Create test utilities** - Helper functions for dashboard tests 13. **Set up visual regression tests** - Capture baseline screenshots 14. **Set up performance monitoring** - Add performance markers 15. **Document test strategy** - Write test plan document ### Phase 2: Migrate Entity List Components (Day 2-3) **Step 16-20: Migrate CourseList** 16. **Read CourseList.tsx** - Understand current implementation 17. **Replace useQuery with useThings** - Migrate to DataProvider hook 18. **Update loading state** - Use `loading` from hook 19. **Update error handling** - Use `error` from hook 20. **Test CourseList** - Run component tests, verify functionality **Step 21-25: Migrate LessonList** 21. **Read LessonList.tsx** - Understand current implementation 22. **Replace useQuery with useThings** - Migrate to DataProvider hook 23. **Add courseId filter** - Filter lessons by parent course 24. **Update loading state** - Use `loading` from hook 25. **Test LessonList** - Run component tests, verify functionality **Step 26-30: Migrate StudentList** 26. **Read StudentList.tsx** - Understand current implementation 27. **Replace useQuery with useThings** - Migrate to DataProvider hook 28. **Update loading state** - Use `loading` from hook 29. **Add pagination support** - Implement load more functionality 30. **Test StudentList** - Run component tests, verify functionality ### Phase 3: Migrate Entity Detail Components (Day 3-4) **Step 31-35: Migrate CourseDetail** 31. **Read CourseDetail.tsx** - Understand current implementation 32. **Replace useQuery with useThing** - Migrate single entity fetch 33. **Add useConnections for students** - Fetch enrolled students 34. **Replace useMutation with useUpdateThing** - Migrate update logic 35. **Test CourseDetail** - Run component tests, verify functionality **Step 36-40: Migrate LessonDetail** 36. **Read LessonDetail.tsx** - Understand current implementation 37. **Replace useQuery with useThing** - Migrate single entity fetch 38. **Add useConnections for parent course** - Fetch parent course 39. **Replace useMutation with useUpdateThing** - Migrate update logic 40. **Test LessonDetail** - Run component tests, verify functionality ### Phase 4: Migrate Activity & Search (Day 5) **Step 41-45: Migrate ActivityFeed and SearchBar** 41. **Read ActivityFeed.tsx** - Understand current implementation 42. **Replace useQuery with useEvents** - Migrate event stream 43. **Test ActivityFeed** - Run component tests, verify real-time updates 44. **Read SearchBar.tsx** - Understand current implementation 45. **Replace useQuery with useSearch** - Migrate search logic ### Phase 5: Migrate Analytics Components (Day 5-6) **Step 46-50: Migrate DashboardOverview, StatsCards, Charts** 46. **Read DashboardOverview.tsx** - Understand current implementation with multiple hooks 47. **Replace queries with DataProvider hooks** - Migrate all data fetching 48. **Add client-side aggregation** - Calculate metrics from raw data 49. **Update chart components** - Ensure charts work with new data format 50. **Test DashboardOverview** - Run component tests, verify all metrics display ### Phase 6: Cleanup and Optimization (Day 6-7) **Step 51-60: Remove Dependencies and Optimize** 51. **Remove all Convex imports** - Grep and remove `from "convex/react"` 52. **Remove api imports** - Remove `import { api } from "@/convex/_generated/api"` 53. **Add error boundaries** - Wrap components with error boundaries 54. **Optimize performance** - Add memoization, virtualization where needed 55. **Update all tests** - Ensure all tests use DataProvider mocks 56. **Run full test suite** - Verify all tests pass 57. **Run visual regression tests** - Compare screenshots 58. **Run performance tests** - Verify within 10% of baseline 59. **Document migration changes** - Update component API docs 60. **Create pull request** - Submit for review --- ## 5. Complete Code Examples ### Example 1: DashboardOverview Migration **BEFORE (Tightly Coupled to Convex):** ```typescript // frontend/src/components/dashboard/DashboardOverview.tsx import { useQuery } from "convex/react"; import { api } from "@/convex/_generated/api"; import { StatsCards } from "./StatsCards"; import { ActivityCharts } from "./ActivityCharts"; import { RevenueCharts } from "./RevenueCharts"; import { RecentTransactions } from "./RecentTransactions"; export function DashboardOverview() { // Multiple Convex queries const courses = useQuery(api.queries.entities.list, { type: "course" }); const students = useQuery(api.queries.entities.list, { type: "student" }); const recentEvents = useQuery(api.queries.events.getRecent, { limit: 10 }); const transactions = useQuery(api.queries.events.getByType, { eventType: "token_transferred", limit: 10 }); // Loading state if (courses === undefined || students === undefined) { return <div>Loading dashboard...</div>; } // Aggregate metrics const totalCourses = courses.length; const totalStudents = students.length; const activeStudents = students.filter(s => s.properties.lastActive > Date.now() - 7 * 24 * 60 * 60 * 1000).length; return ( <div className="space-y-6"> <h1 className="text-3xl font-bold">Dashboard</h1> <StatsCards totalCourses={totalCourses} totalStudents={totalStudents} activeStudents={activeStudents} /> <div className="grid grid-cols-2 gap-6"> <ActivityCharts events={recentEvents || []} /> <RevenueCharts transactions={transactions || []} /> </div> <RecentTransactions transactions={transactions || []} /> </div> ); } ``` **AFTER (Backend-Agnostic with DataProvider):** ```typescript // frontend/src/components/dashboard/DashboardOverview.tsx import { useMemo } from "react"; import { useThings, useEvents } from "@/providers/hooks"; import { StatsCards } from "./StatsCards"; import { ActivityCharts } from "./ActivityCharts"; import { RevenueCharts } from "./RevenueCharts"; import { RecentTransactions } from "./RecentTransactions"; import { LoadingSkeleton } from "@/components/ui/loading-skeleton"; import { ErrorMessage } from "@/components/ui/error-message"; export function DashboardOverview() { // DataProvider hooks (backend-agnostic) const { data: courses, loading: coursesLoading, error: coursesError } = useThings({ type: "course" }); const { data: students, loading: studentsLoading, error: studentsError } = useThings({ type: "student" }); const { data: recentEvents, loading: eventsLoading } = useEvents({ limit: 10 }); const { data: transactions, loading: transactionsLoading } = useEvents({ eventType: "token_transferred", limit: 10 }); // Aggregate loading state const loading = coursesLoading || studentsLoading; // Handle errors if (coursesError || studentsError) { return <ErrorMessage error={coursesError || studentsError} />; } // Loading state with skeleton if (loading) { return <LoadingSkeleton layout="dashboard" />; } // Memoized metrics (only recalculate when data changes) const metrics = useMemo(() => { const totalCourses = courses?.length || 0; const totalStudents = students?.length || 0; const activeStudents = students?.filter( s => s.properties.lastActive > Date.now() - 7 * 24 * 60 * 60 * 1000 ).length || 0; return { totalCourses, totalStudents, activeStudents }; }, [courses, students]); return ( <div className="space-y-6"> <h1 className="text-3xl font-bold">Dashboard</h1> <StatsCards totalCourses={metrics.totalCourses} totalStudents={metrics.totalStudents} activeStudents={metrics.activeStudents} /> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> {eventsLoading ? ( <LoadingSkeleton layout="chart" /> ) : ( <ActivityCharts events={recentEvents || []} /> )} {transactionsLoading ? ( <LoadingSkeleton layout="chart" /> ) : ( <RevenueCharts transactions={transactions || []} /> )} </div> {transactionsLoading ? ( <LoadingSkeleton layout="table" /> ) : ( <RecentTransactions transactions={transactions || []} /> )} </div> ); } ``` **Key Changes:** 1. Replaced `useQuery` with `useThings` and `useEvents` 2. Added granular loading states for each data source 3. Added error handling with ErrorMessage component 4. Added memoization for expensive calculations 5. Added responsive grid layout 6. Added proper loading skeletons --- ### Example 2: ActivityCharts Migration **BEFORE (Receives Pre-Aggregated Data):** ```typescript // frontend/src/components/dashboard/ActivityCharts.tsx import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts"; import type { Event } from "@/convex/_generated/dataModel"; interface ActivityChartsProps { events: Event[]; } export function ActivityCharts({ events }: ActivityChartsProps) { // Simple data transformation const chartData = events.map(event => ({ timestamp: new Date(event.timestamp).toLocaleDateString(), count: 1 })); return ( <div className="bg-white p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold mb-4">Recent Activity</h3> <ResponsiveContainer width="100%" height={300}> <LineChart data={chartData}> <XAxis dataKey="timestamp" /> <YAxis /> <Tooltip /> <Line type="monotone" dataKey="count" stroke="#8884d8" /> </LineChart> </ResponsiveContainer> </div> ); } ``` **AFTER (With Client-Side Aggregation and Optimization):** ```typescript // frontend/src/components/dashboard/ActivityCharts.tsx import { useMemo } from "react"; import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts"; import type { Event } from "@/types/ontology"; interface ActivityChartsProps { events: Event[]; } export function ActivityCharts({ events }: ActivityChartsProps) { // Memoized data aggregation const chartData = useMemo(() => { // Group events by day const eventsByDay = events.reduce((acc, event) => { const day = new Date(event.timestamp).toLocaleDateString(); acc[day] = (acc[day] || 0) + 1; return acc; }, {} as Record<string, number>); // Convert to array sorted by date return Object.entries(eventsByDay) .map(([date, count]) => ({ date, count, timestamp: new Date(date).getTime() })) .sort((a, b) => a.timestamp - b.timestamp) .map(({ date, count }) => ({ date, count })); }, [events]); // Event type breakdown const eventTypeData = useMemo(() => { const typeCount = events.reduce((acc, event) => { acc[event.type] = (acc[event.type] || 0) + 1; return acc; }, {} as Record<string, number>); return Object.entries(typeCount) .map(([type, count]) => ({ type, count })) .sort((a, b) => b.count - a.count) .slice(0, 5); // Top 5 event types }, [events]); return ( <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold mb-4">Recent Activity</h3> {/* Timeline Chart */} <div className="mb-6"> <h4 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2"> Activity Over Time </h4> <ResponsiveContainer width="100%" height={250}> <LineChart data={chartData}> <XAxis dataKey="date" tick={{ fontSize: 12 }} tickFormatter={(value) => new Date(value).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} /> <YAxis tick={{ fontSize: 12 }} /> <Tooltip contentStyle={{ backgroundColor: 'hsl(var(--color-background))', border: '1px solid hsl(var(--color-border))', borderRadius: '8px' }} /> <Line type="monotone" dataKey="count" stroke="hsl(var(--color-primary))" strokeWidth={2} dot={{ r: 4 }} activeDot={{ r: 6 }} /> </LineChart> </ResponsiveContainer> </div> {/* Event Type Breakdown */} <div> <h4 className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2"> Top Event Types </h4> <div className="space-y-2"> {eventTypeData.map(({ type, count }) => ( <div key={type} className="flex justify-between items-center"> <span className="text-sm">{type.replace(/_/g, ' ')}</span> <span className="text-sm font-semibold">{count}</span> </div> ))} </div> </div> </div> ); } ``` **Key Changes:** 1. Added memoization for expensive aggregation operations 2. Added event type breakdown visualization 3. Improved chart styling with theme colors 4. Added dark mode support 5. Improved date formatting 6. Removed dependency on Convex types --- ### Example 3: RevenueCharts Migration **BEFORE:** ```typescript // frontend/src/components/dashboard/RevenueCharts.tsx import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts"; import type { Event } from "@/convex/_generated/dataModel"; interface RevenueChartsProps { transactions: Event[]; } export function RevenueCharts({ transactions }: RevenueChartsProps) { const revenueData = transactions.map(tx => ({ date: new Date(tx.timestamp).toLocaleDateString(), revenue: tx.metadata.amount || 0 })); return ( <div className="bg-white p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold mb-4">Revenue</h3> <ResponsiveContainer width="100%" height={300}> <BarChart data={revenueData}> <XAxis dataKey="date" /> <YAxis /> <Tooltip /> <Bar dataKey="revenue" fill="#82ca9d" /> </BarChart> </ResponsiveContainer> </div> ); } ``` **AFTER:** ```typescript // frontend/src/components/dashboard/RevenueCharts.tsx import { useMemo, useState } from "react"; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from "recharts"; import type { Event } from "@/types/ontology"; interface RevenueChartsProps { transactions: Event[]; } type TimeRange = "day" | "week" | "month"; export function RevenueCharts({ transactions }: RevenueChartsProps) { const [timeRange, setTimeRange] = useState<TimeRange>("week"); // Memoized revenue aggregation by time range const revenueData = useMemo(() => { // Group by time range const revenueByPeriod = transactions.reduce((acc, tx) => { const date = new Date(tx.timestamp); let key: string; switch (timeRange) { case "day": key = date.toLocaleDateString(); break; case "week": const weekStart = new Date(date); weekStart.setDate(date.getDate() - date.getDay()); key = weekStart.toLocaleDateString(); break; case "month": key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; } const amount = tx.metadata?.amount || 0; acc[key] = (acc[key] || 0) + amount; return acc; }, {} as Record<string, number>); // Convert to array and sort return Object.entries(revenueByPeriod) .map(([period, revenue]) => ({ period, revenue })) .sort((a, b) => new Date(a.period).getTime() - new Date(b.period).getTime()); }, [transactions, timeRange]); // Calculate summary metrics const metrics = useMemo(() => { const total = revenueData.reduce((sum, item) => sum + item.revenue, 0); const average = total / (revenueData.length || 1); const max = Math.max(...revenueData.map(item => item.revenue)); return { total, average, max }; }, [revenueData]); return ( <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow"> <div className="flex justify-between items-center mb-4"> <h3 className="text-lg font-semibold">Revenue</h3> {/* Time Range Selector */} <div className="flex gap-2"> {(["day", "week", "month"] as TimeRange[]).map(range => ( <button key={range} onClick={() => setTimeRange(range)} className={`px-3 py-1 rounded text-sm ${ timeRange === range ? "bg-blue-600 text-white" : "bg-gray-200 text-gray-700 hover:bg-gray-300" }`} > {range.charAt(0).toUpperCase() + range.slice(1)} </button> ))} </div> </div> {/* Summary Metrics */} <div className="grid grid-cols-3 gap-4 mb-6"> <div className="text-center"> <div className="text-2xl font-bold text-green-600"> ${metrics.total.toLocaleString()} </div> <div className="text-xs text-gray-600 dark:text-gray-400">Total Revenue</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-blue-600"> ${metrics.average.toLocaleString()} </div> <div className="text-xs text-gray-600 dark:text-gray-400">Average</div> </div> <div className="text-center"> <div className="text-2xl font-bold text-purple-600"> ${metrics.max.toLocaleString()} </div> <div className="text-xs text-gray-600 dark:text-gray-400">Peak</div> </div> </div> {/* Chart */} <ResponsiveContainer width="100%" height={250}> <BarChart data={revenueData}> <XAxis dataKey="period" tick={{ fontSize: 12 }} tickFormatter={(value) => { const date = new Date(value); return timeRange === "month" ? date.toLocaleDateString(undefined, { month: 'short', year: 'numeric' }) : date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); }} /> <YAxis tick={{ fontSize: 12 }} tickFormatter={(value) => `$${value.toLocaleString()}`} /> <Tooltip formatter={(value: number) => [`$${value.toLocaleString()}`, "Revenue"]} contentStyle={{ backgroundColor: 'hsl(var(--color-background))', border: '1px solid hsl(var(--color-border))', borderRadius: '8px' }} /> <Bar dataKey="revenue" radius={[8, 8, 0, 0]}> {revenueData.map((entry, index) => ( <Cell key={`cell-${index}`} fill={`hsl(${120 + (entry.revenue / metrics.max) * 60}, 70%, 50%)`} /> ))} </Bar> </BarChart> </ResponsiveContainer> </div> ); } ``` **Key Changes:** 1. Added time range selector (day/week/month) 2. Added summary metrics (total, average, peak) 3. Added dynamic bar coloring based on revenue amount 4. Added memoization for aggregation 5. Improved date formatting based on time range 6. Added dark mode support --- ### Example 4: RecentTransactions Migration **BEFORE:** ```typescript // frontend/src/components/dashboard/RecentTransactions.tsx import type { Event } from "@/convex/_generated/dataModel"; interface RecentTransactionsProps { transactions: Event[]; } export function RecentTransactions({ transactions }: RecentTransactionsProps) { return ( <div className="bg-white p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold mb-4">Recent Transactions</h3> <table className="w-full"> <thead> <tr> <th>Date</th> <th>Amount</th> <th>Type</th> </tr> </thead> <tbody> {transactions.map(tx => ( <tr key={tx._id}> <td>{new Date(tx.timestamp).toLocaleDateString()}</td> <td>${tx.metadata.amount}</td> <td>{tx.type}</td> </tr> ))} </tbody> </table> </div> ); } ``` **AFTER:** ```typescript // frontend/src/components/dashboard/RecentTransactions.tsx import { useMemo } from "react"; import { useThing } from "@/providers/hooks"; import type { Event } from "@/types/ontology"; import type { Id } from "@/types/convex"; interface RecentTransactionsProps { transactions: Event[]; } function TransactionRow({ transaction }: { transaction: Event }) { // Fetch actor and target entities for display const { data: actor } = useThing({ id: transaction.actorId as Id<"things"> }); const { data: target } = useThing({ id: transaction.targetId as Id<"things"> }); const amount = transaction.metadata?.amount || 0; const transactionType = transaction.type.replace(/_/g, ' '); return ( <tr className="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800"> <td className="py-3 px-4"> <div className="text-sm font-medium"> {new Date(transaction.timestamp).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })} </div> <div className="text-xs text-gray-500"> {new Date(transaction.timestamp).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })} </div> </td> <td className="py-3 px-4"> <div className="text-sm font-semibold text-green-600">${amount.toLocaleString()}</div> </td> <td className="py-3 px-4"> <div className="text-sm">{transactionType}</div> </td> <td className="py-3 px-4"> <div className="text-sm text-gray-600 dark:text-gray-400"> {actor?.name || "Unknown"} </div> </td> <td className="py-3 px-4"> <div className="text-sm text-gray-600 dark:text-gray-400"> {target?.name || "Unknown"} </div> </td> <td className="py-3 px-4"> <span className={`px-2 py-1 rounded-full text-xs font-medium ${ transaction.metadata?.status === "completed" ? "bg-green-100 text-green-800" : transaction.metadata?.status === "pending" ? "bg-yellow-100 text-yellow-800" : "bg-red-100 text-red-800" }`}> {transaction.metadata?.status || "completed"} </span> </td> </tr> ); } export function RecentTransactions({ transactions }: RecentTransactionsProps) { // Sort by most recent const sortedTransactions = useMemo(() => { return [...transactions].sort((a, b) => b.timestamp - a.timestamp); }, [transactions]); // Calculate total const totalRevenue = useMemo(() => { return transactions.reduce((sum, tx) => sum + (tx.metadata?.amount || 0), 0); }, [transactions]); return ( <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow"> <div className="flex justify-between items-center mb-4"> <h3 className="text-lg font-semibold">Recent Transactions</h3> <div className="text-sm text-gray-600 dark:text-gray-400"> Total: <span className="font-semibold text-green-600">${totalRevenue.toLocaleString()}</span> </div> </div> {sortedTransactions.length === 0 ? ( <div className="text-center py-8 text-gray-500"> No transactions yet </div> ) : ( <div className="overflow-x-auto"> <table className="w-full"> <thead> <tr className="border-b border-gray-300 dark:border-gray-600"> <th className="py-2 px-4 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> Date </th> <th className="py-2 px-4 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> Amount </th> <th className="py-2 px-4 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> Type </th> <th className="py-2 px-4 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> From </th> <th className="py-2 px-4 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> To </th> <th className="py-2 px-4 text-left text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider"> Status </th> </tr> </thead> <tbody> {sortedTransactions.map(transaction => ( <TransactionRow key={transaction._id} transaction={transaction} /> ))} </tbody> </table> </div> )} </div> ); } ``` **Key Changes:** 1. Added TransactionRow component with entity lookups 2. Added sorting by timestamp 3. Added total revenue calculation 4. Added status badges with colors 5. Added empty state 6. Improved table styling with dark mode 7. Added actor and target entity display --- ### Example 5: QuickActions Component **NEW Component (Not in Original Spec):** ```typescript // frontend/src/components/dashboard/QuickActions.tsx import { useState } from "react"; import { useCreateThing, useCreateConnection } from "@/providers/hooks"; import { useAuth } from "@/providers/auth"; import { PlusIcon, UserPlusIcon, FileTextIcon, BotIcon } from "lucide-react"; import { toast } from "sonner"; import type { ThingType } from "@/types/ontology"; export function QuickActions() { const { user } = useAuth(); const [creating, setCreating] = useState(false); const createThing = useCreateThing(); const createConnection = useCreateConnection(); const handleCreateEntity = async (type: ThingType, name: string) => { setCreating(true); try { const entityId = await createThing({ type, name, properties: {}, status: "draft" }); // Create ownership connection if (user) { await createConnection({ fromThingId: user.id, toThingId: entityId, relationshipType: "owns", metadata: { createdVia: "quick_action" } }); } toast.success(`${name} created successfully!`); } catch (error) { toast.error(`Failed to create ${name}`); console.error(error); } finally { setCreating(false); } }; const actions = [ { icon: FileTextIcon, label: "New Course", color: "blue", onClick: () => handleCreateEntity("course", "New Course") }, { icon: BotIcon, label: "New AI Agent", color: "purple", onClick: () => handleCreateEntity("ai_clone", "New Agent") }, { icon: UserPlusIcon, label: "Invite Student", color: "green", onClick: () => { // Open invite modal toast.info("Opening invite dialog..."); } } ]; return ( <div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow"> <h3 className="text-lg font-semibold mb-4">Quick Actions</h3> <div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> {actions.map(action => { const Icon = action.icon; return ( <button key={action.label} onClick={action.onClick} disabled={creating} className={`flex items-center gap-3 p-4 rounded-lg border-2 border-${action.color}-200 hover:border-${action.color}-400 hover:bg-${action.color}-50 dark:hover:bg-${action.color}-900 transition-colors disabled:opacity-50 disabled:cursor-not-allowed`} > <Icon className={`w-6 h-6 text-${action.color}-600`} /> <span className="font-medium">{action.label}</span> </button> ); })} </div> </div> ); } ``` --- ## 6. Testing Strategy ### Unit Tests **Test each component independently with mocked DataProvider hooks:** ```typescript // frontend/test/components/dashboard/CourseList.test.tsx import { describe, it, expect, vi } from "vitest"; import { render, screen } from "@testing-library/react"; import { CourseList } from "@/components/dashboard/CourseList"; import * as hooks from "@/providers/hooks"; describe("CourseList", () => { it("displays loading state", () => { vi.spyOn(hooks, "useThings").mockReturnValue({ data: undefined, loading: true, error: null }); render(<CourseList />); expect(screen.getByText(/loading/i)).toBeInTheDocument(); }); it("displays courses when loaded", () => { vi.spyOn(hooks, "useThings").mockReturnValue({ data: [ { _id: "1", type: "course", name: "Course 1", properties: {}, status: "published" }, { _id: "2", type: "course", name: "Course 2", properties: {}, status: "published" } ], loading: false, error: null }); render(<CourseList />); expect(screen.getByText("Course 1")).toBeInTheDocument(); expect(screen.getByText("Course 2")).toBeInTheDocument(); }); it("displays error state", () => { vi.spyOn(hooks, "useThings").mockReturnValue({ data: undefined, loading: false, error: new Error("Failed to load courses") }); render(<CourseList />); expect(screen.getByText(/failed to load/i)).toBeInTheDocument(); }); }); ``` ### Integration Tests **Test dashboard with full DataProvider context:** ```typescript // frontend/test/integration/dashboard.test.tsx import { describe, it, expect } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import { DataProvider } from "@/providers/data-provider"; import { DashboardOverview } from "@/components/dashboard/DashboardOverview"; describe("Dashboard Integration", () => { it("loads dashboard with all data", async () => { render( <DataProvider> <DashboardOverview /> </DataProvider> ); // Wait for loading to complete await waitFor(() => { expect(screen.queryByText(/loading/i)).not.toBeInTheDocument(); }); // Verify all sections render expect(screen.getByText(/total courses/i)).toBeInTheDocument(); expect(screen.getByText(/total students/i)).toBeInTheDocument(); expect(screen.getByText(/recent activity/i)).toBeInTheDocument(); expect(screen.getByText(/revenue/i)).toBeInTheDocument(); }); }); ``` ### Visual Regression Tests **Capture screenshots and compare:** ```typescript // frontend/test/visual/dashboard.visual.test.tsx import { test, expect } from "@playwright/test"; test("dashboard matches baseline", async ({ page }) => { await page.goto("http://localhost:4321/dashboard"); // Wait for loading to complete await page.waitForSelector('[data-testid="dashboard-loaded"]'); // Capture screenshot await expect(page).toHaveScreenshot("dashboard-overview.png"); }); test("dashboard dark mode matches baseline", async ({ page }) => { await page.goto("http://localhost:4321/dashboard"); await page.waitForSelector('[data-testid="dashboard-loaded"]'); // Enable dark mode await page.click('[data-testid="theme-toggle"]'); // Capture screenshot await expect(page).toHaveScreenshot("dashboard-overview-dark.png"); }); ``` ### Performance Tests **Measure and validate performance:** ```typescript // frontend/test/performance/dashboard.perf.test.ts import { test, expect } from "@playwright/test"; test("dashboard loads within performance budget", async ({ page }) => {