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
Markdown
---
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 }) => {