diff --git a/PR_SUMMARY.md b/PR_SUMMARY.md index e82bab29..ea1e6661 100644 --- a/PR_SUMMARY.md +++ b/PR_SUMMARY.md @@ -3,29 +3,36 @@ ## Issue #84: Implement Advanced Data Visualization ### Overview + This PR implements a comprehensive data visualization system for the TeachLink platform with interactive charts, real-time updates, custom chart builder, and data exploration tools. ### Changes Made #### New Components (4) + 1. **InteractiveChartLibrary** - Multi-type chart library with 7 chart types 2. **RealTimeDataVisualizer** - Live data visualization with WebSocket support 3. **CustomVisualizationBuilder** - User-friendly chart creation interface 4. **DataExplorationTools** - Interactive data analysis and filtering #### New Hooks (1) + 1. **useDataVisualization** - Centralized state management for visualizations #### New Utilities (1) + 1. **visualizationUtils** - 20+ helper functions for data transformation and analysis #### New Pages (1) + 1. **Visualization Demo** - Interactive showcase at `/visualization-demo` #### Tests (1) + 1. **visualizationUtils.test.ts** - 25+ test cases with comprehensive coverage #### Documentation (3) + 1. **README.md** - Complete API documentation 2. **QUICK_START.md** - 5-minute getting started guide 3. **VISUALIZATION_IMPLEMENTATION.md** - Implementation details @@ -59,6 +66,7 @@ src/ ### Features Implemented #### ✅ Chart Library + - 7 chart types (Line, Bar, Area, Pie, Doughnut, Scatter, Radar) - Interactive tooltips and legends - Click event handlers @@ -67,6 +75,7 @@ src/ - Responsive design #### ✅ Real-Time Visualization + - WebSocket integration for live updates - Automatic reconnection handling - Data simulation fallback @@ -75,6 +84,7 @@ src/ - Real-time statistics (mean, median, trend) #### ✅ Custom Chart Builder + - Add/remove labels and datasets - Real-time data editing - Live preview @@ -83,6 +93,7 @@ src/ - Export configuration to JSON #### ✅ Data Exploration + - Time range filtering (7d, 30d, 90d, 1y, all) - Chart type switching - Dataset selection @@ -92,6 +103,7 @@ src/ - Responsive statistics cards #### ✅ Additional Features + - Dark mode support - Full TypeScript types - Accessibility (WCAG 2.1 Level AA) @@ -145,6 +157,7 @@ src/ ### Demo Visit `/visualization-demo` to see: + - Interactive chart library with all 7 chart types - Real-time data visualization with live updates - Custom chart builder with full editing capabilities @@ -153,17 +166,15 @@ Visit `/visualization-demo` to see: ### Usage Examples #### Basic Chart + ```tsx import { InteractiveChartLibrary } from '@/components/visualization'; - +; ``` #### Real-Time Data + ```tsx import { RealTimeDataVisualizer } from '@/components/visualization'; @@ -171,26 +182,23 @@ import { RealTimeDataVisualizer } from '@/components/visualization'; websocketUrl="wss://api.example.com/data" chartType="area" title="Live Activity" -/> +/>; ``` #### Custom Builder + ```tsx import { CustomVisualizationBuilder } from '@/components/visualization'; - saveChart(config)} -/> + saveChart(config)} />; ``` #### Data Exploration + ```tsx import { DataExplorationTools } from '@/components/visualization'; - +; ``` ### Acceptance Criteria @@ -206,6 +214,7 @@ All acceptance criteria from issue #84 have been met: ### Documentation Comprehensive documentation provided: + - API reference for all components - Usage examples and code snippets - Best practices guide @@ -271,6 +280,7 @@ Closes #84 ### Future Enhancements Potential improvements for future PRs: + - 3D chart support - Heatmap visualizations - Gantt charts for course timelines @@ -283,6 +293,7 @@ Potential improvements for future PRs: ### Questions? For questions or clarifications, please: + 1. Check the comprehensive README 2. Review the demo page at `/visualization-demo` 3. Read the implementation guide diff --git a/VISUALIZATION_IMPLEMENTATION.md b/VISUALIZATION_IMPLEMENTATION.md index 18121b97..fa45100d 100644 --- a/VISUALIZATION_IMPLEMENTATION.md +++ b/VISUALIZATION_IMPLEMENTATION.md @@ -9,12 +9,14 @@ This document describes the implementation of advanced data visualization compon ### Components Created 1. **InteractiveChartLibrary** (`src/components/visualization/InteractiveChartLibrary.tsx`) + - Comprehensive chart library with 7 chart types - Interactive features with click handlers - Customizable styling and animations - Responsive design with dark mode support 2. **RealTimeDataVisualizer** (`src/components/visualization/RealTimeDataVisualizer.tsx`) + - Live data updates via WebSocket - Automatic data simulation fallback - Pause/resume functionality @@ -22,6 +24,7 @@ This document describes the implementation of advanced data visualization compon - Connection status monitoring 3. **CustomVisualizationBuilder** (`src/components/visualization/CustomVisualizationBuilder.tsx`) + - User-friendly chart builder interface - Add/remove labels and datasets - Real-time data editing @@ -38,6 +41,7 @@ This document describes the implementation of advanced data visualization compon ### Hooks Created **useDataVisualization** (`src/hooks/useDataVisualization.tsx`) + - Centralized state management for visualizations - WebSocket integration for real-time updates - Auto-refresh functionality @@ -47,6 +51,7 @@ This document describes the implementation of advanced data visualization compon ### Utilities Created **visualizationUtils** (`src/utils/visualizationUtils.ts`) + - Number and percentage formatting - Date label generation - Data aggregation and transformation @@ -60,6 +65,7 @@ This document describes the implementation of advanced data visualization compon ### Demo Page **Visualization Demo** (`src/app/visualization-demo/page.tsx`) + - Interactive showcase of all components - Multiple examples for each chart type - Real-time data demonstrations @@ -69,6 +75,7 @@ This document describes the implementation of advanced data visualization compon ### Tests **Visualization Utils Tests** (`src/utils/__tests__/visualizationUtils.test.ts`) + - Comprehensive test coverage for utility functions - 25+ test cases covering all major functions - Edge case handling @@ -77,6 +84,7 @@ This document describes the implementation of advanced data visualization compon ### Documentation **README** (`src/components/visualization/README.md`) + - Complete API documentation - Usage examples for all components - Best practices guide @@ -86,6 +94,7 @@ This document describes the implementation of advanced data visualization compon ## Features Implemented ### ✅ Chart Library + - [x] Line charts - [x] Bar charts - [x] Area charts @@ -99,6 +108,7 @@ This document describes the implementation of advanced data visualization compon - [x] Smooth animations ### ✅ Real-Time Visualization + - [x] WebSocket integration - [x] Live data streaming - [x] Automatic reconnection @@ -109,6 +119,7 @@ This document describes the implementation of advanced data visualization compon - [x] Trend analysis ### ✅ Custom Chart Builder + - [x] Add/remove labels - [x] Add/remove datasets - [x] Edit data values @@ -119,6 +130,7 @@ This document describes the implementation of advanced data visualization compon - [x] Export to JSON ### ✅ Data Exploration + - [x] Time range filtering - [x] Chart type switching - [x] Dataset selection @@ -129,6 +141,7 @@ This document describes the implementation of advanced data visualization compon - [x] Responsive statistics cards ### ✅ Additional Features + - [x] Dark mode support - [x] Responsive design - [x] Accessibility features @@ -180,15 +193,17 @@ import { InteractiveChartLibrary } from '@/components/visualization'; +/>; ``` ### Real-Time Data @@ -201,7 +216,7 @@ import { RealTimeDataVisualizer } from '@/components/visualization'; chartType="area" title="Live Activity" updateInterval={2000} -/> +/>; ``` ### Custom Builder @@ -209,9 +224,7 @@ import { RealTimeDataVisualizer } from '@/components/visualization'; ```tsx import { CustomVisualizationBuilder } from '@/components/visualization'; - saveToDatabase(config)} -/> + saveToDatabase(config)} />; ``` ### Data Exploration @@ -219,10 +232,7 @@ import { CustomVisualizationBuilder } from '@/components/visualization'; ```tsx import { DataExplorationTools } from '@/components/visualization'; - +; ``` ## Testing @@ -234,6 +244,7 @@ npm test -- src/utils/__tests__/visualizationUtils.test.ts ``` Test coverage includes: + - Number formatting - Percentage formatting - Date label generation @@ -246,6 +257,7 @@ Test coverage includes: ## Accessibility All components follow WCAG 2.1 Level AA guidelines: + - Keyboard navigation support - ARIA labels and roles - Screen reader compatibility @@ -271,6 +283,7 @@ All components follow WCAG 2.1 Level AA guidelines: ## Future Enhancements Potential improvements for future iterations: + - 3D chart support - Heatmap visualizations - Gantt charts for course timelines @@ -285,6 +298,7 @@ Potential improvements for future iterations: ## Integration Points The visualization components integrate with: + - Course analytics system - Student progress tracking - Real-time activity monitoring @@ -304,16 +318,19 @@ The visualization components integrate with: ## Deployment Notes 1. Ensure all dependencies are installed: + ```bash npm install ``` 2. Build the project: + ```bash npm run build ``` 3. Run tests: + ```bash npm test ``` @@ -354,6 +371,7 @@ Date: March 25, 2026 ## Screenshots Visit `/visualization-demo` to see live examples of: + - Interactive chart library with 7 chart types - Real-time data visualization with live updates - Custom chart builder with drag-and-drop @@ -364,6 +382,7 @@ Visit `/visualization-demo` to see live examples of: This implementation provides a comprehensive, production-ready data visualization solution for the TeachLink platform. All components are fully tested, documented, and ready for integration into the main application. The visualization system is: + - **Scalable**: Handles large datasets efficiently - **Flexible**: Supports multiple chart types and configurations - **Interactive**: Provides rich user interactions diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md index 48554385..f9bd7816 100644 --- a/docs/ACCESSIBILITY.md +++ b/docs/ACCESSIBILITY.md @@ -4,14 +4,14 @@ This app ships a layered accessibility toolkit aimed at **WCAG 2.1 Level AA** pa ## Architecture -| Piece | Role | -| --- | --- | -| `AccessibilityProvider` | Global context: `announce`, motion preference, keyboard modality, `runPageAudit`. | -| `ScreenReaderSupport` | Permanent **polite** and **assertive** live regions for reliable announcements. | -| `KeyboardNavigation` | **Alt+M** focuses main content; **Shift+?** opens a shortcuts dialog (focus-trapped). Toolbar **roving** focus with `[data-roving-root]`. | -| `AccessibilityAudit` | Dev-only (by default) floating panel with heuristic DOM checks. | -| `useAccessibility()` | Reads context, or a safe fallback when used outside the provider. | -| `accessibilityUtils` | Focus helpers, contrast math, `checkAccessibilityIssues`, `runAccessibilityAudit`. | +| Piece | Role | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `AccessibilityProvider` | Global context: `announce`, motion preference, keyboard modality, `runPageAudit`. | +| `ScreenReaderSupport` | Permanent **polite** and **assertive** live regions for reliable announcements. | +| `KeyboardNavigation` | **Alt+M** focuses main content; **Shift+?** opens a shortcuts dialog (focus-trapped). Toolbar **roving** focus with `[data-roving-root]`. | +| `AccessibilityAudit` | Dev-only (by default) floating panel with heuristic DOM checks. | +| `useAccessibility()` | Reads context, or a safe fallback when used outside the provider. | +| `accessibilityUtils` | Focus helpers, contrast math, `checkAccessibilityIssues`, `runAccessibilityAudit`. | ## Using the provider @@ -39,7 +39,7 @@ Use **assertive** only for urgent errors or time-sensitive status. - Give the primary `
` a stable id such as `main-content` so skip links and **Alt+M** work everywhere. There should be **exactly one** `
` (or `role="main"`) per view. - For horizontal toolbars, add `data-roving-root` on the toolbar container. **Left/Right arrow** moves among buttons, links, tabs, and elements marked with `data-roving-item` (including those using `tabindex="-1"` for roving patterns). -## What automation does *not* prove +## What automation does _not_ prove - **WCAG 2.1 AA** for the whole product requires page-by-page review (contrast in context, timing, reflow, errors, etc.). - The audit panel and `checkAccessibilityIssues` only flag **some** DOM patterns. They miss false positives/negatives and cannot judge screen reader UX. diff --git a/package.json b/package.json index 17bd7332..e44abe35 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,9 @@ "build": "next build", "start": "next start", "type-check": "tsc --noEmit", - "lint": "next lint", + "lint": "next lint --max-warnings=0", "validate:ui": "node scripts/validate-ui.js", "validate:web3": "node scripts/validate-web3.js", - "lint": "next lint --max-warnings=0", "lint:fix": "eslint --ext .ts,.tsx,.js,.jsx . --fix", "format": "prettier --write .", "prepare": "husky install", diff --git a/scripts/validate-ui.js b/scripts/validate-ui.js index 827c5b18..95235a02 100644 --- a/scripts/validate-ui.js +++ b/scripts/validate-ui.js @@ -6,11 +6,11 @@ * Exit code 0 = pass, 1 = fail */ -const fs = require('fs'); -const path = require('path'); -const { execSync } = require('child_process'); +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; -const SRC_DIR = path.join(__dirname, '../src'); +const SRC_DIR = path.join(import.meta.dirname, '../src'); const COMPONENT_DIRS = ['components', 'app', 'pages']; // Disallowed icon libraries (should use lucide-react) @@ -108,14 +108,23 @@ function validateFiles() { } function printResults() { + console.log('UI Validation Summary:'); + console.log(`Files checked in: ${COMPONENT_DIRS.join(', ')}`); + console.log(`Found ${warnings.length} warnings and ${errors.length} errors.\n`); + if (warnings.length > 0) { - warnings.forEach((w) => {}); + console.log('--- WARNINGS ---'); + warnings.forEach((w) => console.warn(w)); + console.log(''); } if (errors.length > 0) { - errors.forEach((e) => {}); + console.log('--- ERRORS ---'); + errors.forEach((e) => console.error(e)); process.exit(1); } + + console.log('UI validation passed successfully! ✨'); process.exit(0); } diff --git a/scripts/validate-web3.js b/scripts/validate-web3.js index 1b8afd85..82c768a2 100644 --- a/scripts/validate-web3.js +++ b/scripts/validate-web3.js @@ -6,10 +6,10 @@ * Exit code 0 = pass, 1 = fail */ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; -const SRC_DIR = path.join(__dirname, '../src'); +const SRC_DIR = path.join(import.meta.dirname, '../src'); let errors = []; let warnings = []; @@ -67,8 +67,8 @@ function checkWeb3Utils() { } function checkEnvExample() { - const envExamplePath = path.join(__dirname, '../.env.example'); - const envLocalPath = path.join(__dirname, '../.env.local.example'); + const envExamplePath = path.join(import.meta.dirname, '../.env.example'); + const envLocalPath = path.join(import.meta.dirname, '../.env.local.example'); if (!fs.existsSync(envExamplePath) && !fs.existsSync(envLocalPath)) { warnings.push('[WEB3] Consider adding .env.example with NEXT_PUBLIC_STARKNET_* variables'); diff --git a/src/app/components/courses/InstructorBio.tsx b/src/app/components/courses/InstructorBio.tsx index 37a02ecc..6a9e0009 100644 --- a/src/app/components/courses/InstructorBio.tsx +++ b/src/app/components/courses/InstructorBio.tsx @@ -32,7 +32,14 @@ export default function InstructorBio({
- {name} + {name}

diff --git a/src/app/components/dashboard/DashboardGrid.tsx b/src/app/components/dashboard/DashboardGrid.tsx index b7aa3e36..6c063689 100644 --- a/src/app/components/dashboard/DashboardGrid.tsx +++ b/src/app/components/dashboard/DashboardGrid.tsx @@ -20,14 +20,46 @@ import { } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { Settings, Plus, Grid3X3, Calendar } from 'lucide-react'; -import { ProgressSummaryWidget } from './widgets/ProgressSummaryWidget'; -import { UpcomingDeadlinesWidget } from './widgets/UpcomingDeadlinesWidget'; -import { RecommendedCoursesWidget } from './widgets/RecommendedCoursesWidget'; -import { LearningStreakWidget } from './widgets/LearningStreakWidget'; -import { RecentActivityWidget } from './widgets/RecentActivityWidget'; -import { RecentSalesWidget } from './widgets/RecentSalesWidget'; +import dynamic from 'next/dynamic'; import { useDashboardWidgets } from '../../hooks/useDashboardWidgets'; +const ProgressSummaryWidget = dynamic( + () => import('./widgets/ProgressSummaryWidget').then((mod) => mod.ProgressSummaryWidget), + { + loading: () =>
, + }, +); +const UpcomingDeadlinesWidget = dynamic( + () => import('./widgets/UpcomingDeadlinesWidget').then((mod) => mod.UpcomingDeadlinesWidget), + { + loading: () =>
, + }, +); +const RecommendedCoursesWidget = dynamic( + () => import('./widgets/RecommendedCoursesWidget').then((mod) => mod.RecommendedCoursesWidget), + { + loading: () =>
, + }, +); +const LearningStreakWidget = dynamic( + () => import('./widgets/LearningStreakWidget').then((mod) => mod.LearningStreakWidget), + { + loading: () =>
, + }, +); +const RecentActivityWidget = dynamic( + () => import('./widgets/RecentActivityWidget').then((mod) => mod.RecentActivityWidget), + { + loading: () =>
, + }, +); +const RecentSalesWidget = dynamic( + () => import('./widgets/RecentSalesWidget').then((mod) => mod.RecentSalesWidget), + { + loading: () =>
, + }, +); + interface Widget { id: string; type: string; diff --git a/src/app/components/dashboard/widgets/RecentSalesWidget.tsx b/src/app/components/dashboard/widgets/RecentSalesWidget.tsx index 1020fc7e..b7bd8d3e 100644 --- a/src/app/components/dashboard/widgets/RecentSalesWidget.tsx +++ b/src/app/components/dashboard/widgets/RecentSalesWidget.tsx @@ -4,6 +4,14 @@ import React, { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { DollarSign, Settings } from 'lucide-react'; import { format } from 'date-fns'; +import dynamic from 'next/dynamic'; + +const RevenueChart = dynamic(() => import('./RevenueChart'), { + loading: () => ( +
+ ), + ssr: false, +}); interface Sale { id: string; @@ -230,6 +238,9 @@ export const RecentSalesWidget: React.FC = ({ You made {totalSales} sales this month.

+ {/* Revenue Chart */} + + {/* Sales List */}
{sales.map((sale, index) => ( diff --git a/src/app/components/dashboard/widgets/RevenueChart.tsx b/src/app/components/dashboard/widgets/RevenueChart.tsx new file mode 100644 index 00000000..18de90a0 --- /dev/null +++ b/src/app/components/dashboard/widgets/RevenueChart.tsx @@ -0,0 +1,65 @@ +'use client'; + +import React from 'react'; +import { + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; + +const data = [ + { name: 'Mon', revenue: 400 }, + { name: 'Tue', revenue: 300 }, + { name: 'Wed', revenue: 600 }, + { name: 'Thu', revenue: 800 }, + { name: 'Fri', revenue: 500 }, + { name: 'Sat', revenue: 900 }, + { name: 'Sun', revenue: 1000 }, +]; + +export const RevenueChart = () => { + return ( +
+ + + + + + + + + + + + + + + +
+ ); +}; + +export default RevenueChart; diff --git a/src/app/components/notifications/MultiChannelDelivery.tsx b/src/app/components/notifications/MultiChannelDelivery.tsx index c06f60f5..69776aaa 100644 --- a/src/app/components/notifications/MultiChannelDelivery.tsx +++ b/src/app/components/notifications/MultiChannelDelivery.tsx @@ -70,7 +70,7 @@ export default function MultiChannelDelivery({ }); const [selectedChannels, setSelectedChannels] = useState>( - new Set(['in-app']) + new Set(['in-app']), ); const [deliveryStatuses, setDeliveryStatuses] = useState([]); const [message, setMessage] = useState(''); @@ -99,9 +99,7 @@ export default function MultiChannelDelivery({ const channels = Array.from(selectedChannels); // Initialize delivery statuses - setDeliveryStatuses( - channels.map((channel) => ({ channel, status: 'sending' })) - ); + setDeliveryStatuses(channels.map((channel) => ({ channel, status: 'sending' }))); try { // Create the notification @@ -122,10 +120,8 @@ export default function MultiChannelDelivery({ channels.map((channel) => ({ channel, status: results[channel] ? 'success' : 'failed', - message: results[channel] - ? 'Delivered successfully' - : 'Delivery failed', - })) + message: results[channel] ? 'Delivered successfully' : 'Delivery failed', + })), ); onDeliveryComplete?.(results); @@ -144,12 +140,20 @@ export default function MultiChannelDelivery({ channel, status: 'failed', message: 'An error occurred', - })) + })), ); } finally { setIsSending(false); } - }, [message, selectedChannels, category, priority, sendNotification, sendToAllChannels, onDeliveryComplete]); + }, [ + message, + selectedChannels, + category, + priority, + sendNotification, + sendToAllChannels, + onDeliveryComplete, + ]); // Check if channel is enabled in preferences const isChannelEnabled = (channel: NotificationChannel): boolean => { @@ -191,43 +195,47 @@ export default function MultiChannelDelivery({ Select Delivery Channels
- {(Object.entries(channelConfig) as [NotificationChannel, typeof channelConfig['in-app']][]).map( - ([channel, config]) => { - const isSelected = selectedChannels.has(channel); - const isEnabled = isChannelEnabled(channel); + {( + Object.entries(channelConfig) as [ + NotificationChannel, + (typeof channelConfig)['in-app'], + ][] + ).map(([channel, config]) => { + const isSelected = selectedChannels.has(channel); + const isEnabled = isChannelEnabled(channel); - return ( - - ); - } - )} + > +
+ {config.icon} + + {config.label} + + {isSelected && } +
+

{config.description}

+ {!isEnabled && ( +

+ + Disabled in preferences +

+ )} + + ); + })}
@@ -282,9 +290,7 @@ export default function MultiChannelDelivery({ {/* Delivery Status */} {deliveryStatuses.length > 0 && (
- +
{deliveryStatuses.map((status) => { const config = channelConfig[status.channel]; diff --git a/src/app/components/notifications/NotificationCenter.tsx b/src/app/components/notifications/NotificationCenter.tsx index 7a299b34..1307de87 100644 --- a/src/app/components/notifications/NotificationCenter.tsx +++ b/src/app/components/notifications/NotificationCenter.tsx @@ -70,9 +70,7 @@ export default function NotificationCenter({ // Apply search filter if (searchQuery) { const query = searchQuery.toLowerCase(); - result = result.filter((n) => - n.message.toLowerCase().includes(query) - ); + result = result.filter((n) => n.message.toLowerCase().includes(query)); } // Apply read filter @@ -149,7 +147,8 @@ export default function NotificationCenter({ setSortBy('newest'); }; - const hasActiveFilters = searchQuery || filterRead !== 'all' || filterCategory !== 'all' || sortBy !== 'newest'; + const hasActiveFilters = + searchQuery || filterRead !== 'all' || filterCategory !== 'all' || sortBy !== 'newest'; return (
@@ -224,10 +223,7 @@ export default function NotificationCenter({
Filters {hasActiveFilters && ( - )} @@ -321,15 +317,10 @@ export default function NotificationCenter({

- {hasActiveFilters - ? 'No notifications match your filters' - : "You're all caught up!"} + {hasActiveFilters ? 'No notifications match your filters' : "You're all caught up!"}

{hasActiveFilters && ( - )} diff --git a/src/app/components/notifications/NotificationTemplates.tsx b/src/app/components/notifications/NotificationTemplates.tsx index e7b11c0e..bfe8afc0 100644 --- a/src/app/components/notifications/NotificationTemplates.tsx +++ b/src/app/components/notifications/NotificationTemplates.tsx @@ -119,8 +119,7 @@ export default function NotificationTemplates({ template.title.toLowerCase().includes(searchQuery.toLowerCase()) || template.body.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesCategory = - filterCategory === 'all' || template.category === filterCategory; + const matchesCategory = filterCategory === 'all' || template.category === filterCategory; return matchesSearch && matchesCategory; }); @@ -142,9 +141,7 @@ export default function NotificationTemplates({ const handleSaveTemplate = (template: NotificationTemplate) => { if (editingTemplate) { // Update existing - setTemplates((prev) => - prev.map((t) => (t.id === template.id ? template : t)) - ); + setTemplates((prev) => prev.map((t) => (t.id === template.id ? template : t))); onTemplateUpdate?.(template); } else { // Create new @@ -275,10 +272,7 @@ export default function NotificationTemplates({ const preview = getPreviewContent(template); return ( -
+
@@ -340,9 +334,7 @@ export default function NotificationTemplates({ ))}
diff --git a/src/app/components/notifications/UserPreferences.tsx b/src/app/components/notifications/UserPreferences.tsx index 8856609b..08ecc8fa 100644 --- a/src/app/components/notifications/UserPreferences.tsx +++ b/src/app/components/notifications/UserPreferences.tsx @@ -69,7 +69,9 @@ const channelIcons: Record = { export default function UserPreferences({ userId, onSave }: UserPreferencesProps) { const { preferences, updatePreferences, isLoading } = useNotifications({ userId }); - const [localPreferences, setLocalPreferences] = useState(null); + const [localPreferences, setLocalPreferences] = useState( + null, + ); const [hasChanges, setHasChanges] = useState(false); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle'); const [errors, setErrors] = useState([]); @@ -116,7 +118,7 @@ export default function UserPreferences({ userId, onSave }: UserPreferencesProps const toggleCategory = (category: NotificationCategory) => { updateLocalPref( `categories.${category}.enabled`, - !localPreferences?.categories[category]?.enabled + !localPreferences?.categories[category]?.enabled, ); }; @@ -202,9 +204,10 @@ export default function UserPreferences({ userId, onSave }: UserPreferencesProps onClick={() => toggleChannel(channel)} className={` p-3 rounded-lg border-2 text-left transition-all - ${isEnabled - ? 'bg-blue-50 border-blue-200' - : 'bg-white border-gray-200 hover:border-gray-300' + ${ + isEnabled + ? 'bg-blue-50 border-blue-200' + : 'bg-white border-gray-200 hover:border-gray-300' } `} > @@ -212,15 +215,23 @@ export default function UserPreferences({ userId, onSave }: UserPreferencesProps {channelIcons[channel]} - + {channel === 'in-app' ? 'In-App' : channel} -
-
+
+
@@ -237,14 +248,18 @@ export default function UserPreferences({ userId, onSave }: UserPreferencesProps

Quiet Hours

@@ -297,41 +312,48 @@ export default function UserPreferences({ userId, onSave }: UserPreferencesProps

Notification Categories

- {(Object.entries(categoryLabels) as [NotificationCategory, typeof categoryLabels['system']][]).map( - ([category, { label, description }]) => { - const categoryPrefs = localPreferences.categories[category]; - const isEnabled = categoryPrefs?.enabled ?? true; - - return ( -
-
-
-
-
{label}
-
{description}
-
- + {( + Object.entries(categoryLabels) as [ + NotificationCategory, + (typeof categoryLabels)['system'], + ][] + ).map(([category, { label, description }]) => { + const categoryPrefs = localPreferences.categories[category]; + const isEnabled = categoryPrefs?.enabled ?? true; + + return ( +
+
+
+
+
{label}
+
{description}
+ +
- {isEnabled && ( -
-
Channels for this category:
-
- {(['in-app', 'push', 'email', 'sms'] as NotificationChannel[]).map((channel) => { + {isEnabled && ( +
+
Channels for this category:
+
+ {(['in-app', 'push', 'email', 'sms'] as NotificationChannel[]).map( + (channel) => { const isChannelEnabled = categoryPrefs?.channels?.includes(channel); const isGlobalEnabled = localPreferences.channels[channel === 'in-app' ? 'inApp' : channel]; @@ -344,9 +366,10 @@ export default function UserPreferences({ userId, onSave }: UserPreferencesProps className={` inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors - ${isChannelEnabled - ? 'bg-blue-600 text-white' - : 'bg-white text-gray-600 border border-gray-300' + ${ + isChannelEnabled + ? 'bg-blue-600 text-white' + : 'bg-white text-gray-600 border border-gray-300' } ${!isGlobalEnabled ? 'opacity-50 cursor-not-allowed' : ''} `} @@ -357,15 +380,15 @@ export default function UserPreferences({ userId, onSave }: UserPreferencesProps ); - })} -
+ }, + )}
- )} -
+
+ )}
- ); - } - )} +
+ ); + })}
diff --git a/src/app/components/quizzes/QuestionCard.tsx b/src/app/components/quizzes/QuestionCard.tsx index 2051a5cf..1d2eb3c7 100644 --- a/src/app/components/quizzes/QuestionCard.tsx +++ b/src/app/components/quizzes/QuestionCard.tsx @@ -3,7 +3,12 @@ import { Question, useQuizStore } from '@/app/store/quizStore'; import MultipleChoiceQuestion from './question-types/MultipleChoiceQuestion'; import TrueFalseQuestion from './question-types/TrueFalseQuestion'; -import CodeChallengeQuestion from './question-types/CodeChallengeQuestion'; +import dynamic from 'next/dynamic'; + +const CodeChallengeQuestion = dynamic(() => import('./question-types/CodeChallengeQuestion'), { + loading: () =>
, + ssr: false, +}); interface QuestionCardProps { question: Question; diff --git a/src/app/components/shared/ImageUploader.tsx b/src/app/components/shared/ImageUploader.tsx index 659a4ce2..3c266c8b 100644 --- a/src/app/components/shared/ImageUploader.tsx +++ b/src/app/components/shared/ImageUploader.tsx @@ -35,7 +35,14 @@ export default function ImageUploader({ onImageSelect, initialImageUrl }: ImageU onClick={handleClick} > {previewUrl ? ( - Profile preview + Profile preview ) : (
import('@/components/editor/RichContentEditor').then((mod) => mod.RichContentEditor), + { + loading: () => ( +
+ ), + ssr: false, + }, +); export default function EditorPage() { const [content, setContent] = useState('

Start editing...

'); diff --git a/src/app/hooks/useSearchFilters.ts b/src/app/hooks/useSearchFilters.ts index b9f5a98b..35bfd168 100644 --- a/src/app/hooks/useSearchFilters.ts +++ b/src/app/hooks/useSearchFilters.ts @@ -55,7 +55,6 @@ export const useSearchFilters = () => { params.set('q', filters.searchTerm); } - const newUrl = params.toString() ? `${pathname}?${params.toString()}` : (pathname ?? '/'); const newUrl = params.toString() ? `${pathname ?? ''}?${params.toString()}` : pathname ?? ''; router.replace(newUrl, { scroll: false }); }, [filters, pathname, router]); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 496d9b3e..8c867ceb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -33,12 +33,12 @@ export const metadata: Metadata = { manifest: '/manifest.json', }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { - const cookieStore = cookies(); + const cookieStore = await cookies(); const themeCookie = cookieStore.get('theme'); const defaultTheme = themeCookie ? themeCookie.value : 'system'; diff --git a/src/app/mobile/components/OfflineContentManager.tsx b/src/app/mobile/components/OfflineContentManager.tsx index 6e7c638c..0687421f 100644 --- a/src/app/mobile/components/OfflineContentManager.tsx +++ b/src/app/mobile/components/OfflineContentManager.tsx @@ -1,6 +1,5 @@ import { useState, useEffect } from 'react'; import { Download, Check, X, Wifi, WifiOff, Trash2, AlertCircle, RefreshCw } from 'lucide-react'; -import { Download, Check, Wifi, WifiOff, Trash2, AlertCircle, RefreshCw } from 'lucide-react'; import { apiService } from '../services/api'; import { offlineStorage } from '../services/offlineStorage'; import { Course, OfflineContent } from '../types/mobile'; diff --git a/src/app/mobile/components/TouchOptimizedControls.tsx b/src/app/mobile/components/TouchOptimizedControls.tsx index ca54b1c5..560916d5 100644 --- a/src/app/mobile/components/TouchOptimizedControls.tsx +++ b/src/app/mobile/components/TouchOptimizedControls.tsx @@ -1,6 +1,6 @@ -import { useState, useRef, useEffect } from "react"; -import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Maximize, X } from "lucide-react"; -import { offlineStorage } from "../services/offlineStorage"; +import { useState, useRef, useEffect } from 'react'; +import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Maximize, X } from 'lucide-react'; +import { offlineStorage } from '../services/offlineStorage'; interface TouchOptimizedControlsProps { videoTitle: string; diff --git a/src/app/services/offlineSync.ts b/src/app/services/offlineSync.ts index 23778a40..c4f43ae1 100644 --- a/src/app/services/offlineSync.ts +++ b/src/app/services/offlineSync.ts @@ -304,7 +304,7 @@ class OfflineSyncService { const tx = this.db.transaction('conflicts', 'readonly'); const store = tx.objectStore('conflicts'); const index = store.index('resolved'); - + return await index.getAll(false as unknown as IDBValidKey); const all = await index.getAll(); return all.filter((c) => !c.resolved); diff --git a/src/app/store/messagingStore.ts b/src/app/store/messagingStore.ts index e0e7afb4..8f51e046 100644 --- a/src/app/store/messagingStore.ts +++ b/src/app/store/messagingStore.ts @@ -1,7 +1,6 @@ import { create } from 'zustand'; import { io } from 'socket.io-client'; import type { Socket } from 'socket.io-client'; -import io from 'socket.io-client'; export interface Attachment { id: string; diff --git a/src/components/animations/TransitionManager.tsx b/src/components/animations/TransitionManager.tsx index 66474e40..5c777591 100644 --- a/src/components/animations/TransitionManager.tsx +++ b/src/components/animations/TransitionManager.tsx @@ -22,8 +22,8 @@ export async function orchestrateTransitions( for (const item of items) { await item.run(); // small frame gap to allow layout/paint - await new Promise((r) => scheduleFrame(r)) - await new Promise((r) => scheduleFrame(() => r())) + await new Promise((r) => scheduleFrame(r)); + await new Promise((r) => scheduleFrame(() => r())); } } diff --git a/src/components/courses/InstructorBio.tsx b/src/components/courses/InstructorBio.tsx index 803e63e3..958ff82a 100644 --- a/src/components/courses/InstructorBio.tsx +++ b/src/components/courses/InstructorBio.tsx @@ -32,7 +32,14 @@ export default function InstructorBio({

- {name} + {name}

diff --git a/src/components/dashboard/AdvancedDashboard.tsx b/src/components/dashboard/AdvancedDashboard.tsx index ee929a06..ff9bb81a 100644 --- a/src/components/dashboard/AdvancedDashboard.tsx +++ b/src/components/dashboard/AdvancedDashboard.tsx @@ -103,16 +103,19 @@ export const AdvancedDashboard = React.memo(({ className }), ); - const handleDragEnd = useCallback((event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const fromIndex = panels.findIndex((p) => p.id === active.id); - const toIndex = panels.findIndex((p) => p.id === over.id); - if (fromIndex !== -1 && toIndex !== -1) { - reorderPanels(fromIndex, toIndex); - } - }, [panels, reorderPanels]); + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const fromIndex = panels.findIndex((p) => p.id === active.id); + const toIndex = panels.findIndex((p) => p.id === over.id); + if (fromIndex !== -1 && toIndex !== -1) { + reorderPanels(fromIndex, toIndex); + } + }, + [panels, reorderPanels], + ); const handleShare = useCallback(async () => { const url = generateShareURL(); diff --git a/src/components/dashboard/DashboardFilters.tsx b/src/components/dashboard/DashboardFilters.tsx index 21c5ca86..2f269062 100644 --- a/src/components/dashboard/DashboardFilters.tsx +++ b/src/components/dashboard/DashboardFilters.tsx @@ -45,211 +45,225 @@ const DEFAULT_METRICS = ['enrollments', 'revenue', 'completions', 'views']; // ─── Component ──────────────────────────────────────────────────────────────── -export const DashboardFilters = React.memo(({ - filters, - onFiltersChange, - onReset, - categories = DEFAULT_CATEGORIES, - metrics = DEFAULT_METRICS, - className = '', -}) => { - const [isOpen, setIsOpen] = useState(false); - - const toggleCategory = useCallback((cat: string) => { - const next = filters.categories.includes(cat) - ? filters.categories.filter((c) => c !== cat) - : [...filters.categories, cat]; - onFiltersChange({ categories: next }); - }, [filters.categories, onFiltersChange]); - - const removeCategory = useCallback((cat: string) => { - onFiltersChange({ categories: filters.categories.filter((c) => c !== cat) }); - }, [filters.categories, onFiltersChange]); - - const activeFilterCount = useMemo(() => ( - (filters.timeRange !== '30d' ? 1 : 0) + - filters.categories.length + - (filters.metric !== 'enrollments' ? 1 : 0) + - (filters.aggregation !== 'sum' ? 1 : 0) - ), [filters]); - - return ( -
- {/* Header bar */} -
-
- - - {/* Active filter badges */} -
- {filters.timeRange !== '30d' && ( - - {TIME_RANGE_OPTIONS.find((o) => o.value === filters.timeRange)?.label} - + + {/* Active filter badges */} +
+ {filters.timeRange !== '30d' && ( + - - - - )} - {filters.categories.map((cat, i) => ( - - {cat} - + + )} + {filters.categories.map((cat, i) => ( + - - - - ))} + {cat} + + + ))} +
+ + {activeFilterCount > 0 && ( + + )}
- {activeFilterCount > 0 && ( - - )} -
+ {/* Time Range */} +
+ + +
- {/* Expandable panel */} - {isOpen && ( -
- {/* Time Range */} -
- - -
+ {/* Aggregation */} +
+ + +
- {/* Aggregation */} -
- - -
+ {/* Metric */} +
+ + Metric + +
+ {metrics.map((m) => ( + + ))} +
+
- {/* Metric */} -
- - Metric - -
- {metrics.map((m) => ( - - ))} -
-
- - {/* Categories */} -
- - Categories - -
- {categories.map((cat, i) => { - const isActive = filters.categories.includes(cat); - return ( - - ); - })} + {/* Categories */} +
+ + Categories + +
+ {categories.map((cat, i) => { + const isActive = filters.categories.includes(cat); + return ( + + ); + })} +
-
- )} -
- ); -}); + )} +
+ ); + }, +); DashboardFilters.displayName = 'DashboardFilters'; diff --git a/src/components/dashboard/InteractiveCharts.tsx b/src/components/dashboard/InteractiveCharts.tsx index b79548fa..6648b7ea 100644 --- a/src/components/dashboard/InteractiveCharts.tsx +++ b/src/components/dashboard/InteractiveCharts.tsx @@ -49,118 +49,122 @@ const CHART_TYPE_BUTTONS: { type: ChartType; Icon: React.ElementType; label: str // ─── Component ──────────────────────────────────────────────────────────────── -export const InteractiveCharts = React.memo(({ - panelId, - data, - chartType, - title, - drillDownIndex, - onChartTypeChange, - onDrillDown, - onClearDrillDown, - className = '', -}) => { - const isDrillDown = drillDownIndex !== null; - const drillDownData = isDrillDown ? getDrillDownData(data, drillDownIndex) : null; - const drillDownLabel = isDrillDown ? data.labels[drillDownIndex] ?? 'Selected' : null; +export const InteractiveCharts = React.memo( + ({ + panelId, + data, + chartType, + title, + drillDownIndex, + onChartTypeChange, + onDrillDown, + onClearDrillDown, + className = '', + }) => { + const isDrillDown = drillDownIndex !== null; + const drillDownData = isDrillDown ? getDrillDownData(data, drillDownIndex) : null; + const drillDownLabel = isDrillDown ? data.labels[drillDownIndex] ?? 'Selected' : null; - return ( -
- {/* Chart type toolbar */} -
- {CHART_TYPE_BUTTONS.map(({ type, Icon, label }) => ( - - ))} -
- - {/* Main chart */} - - {!isDrillDown ? ( - - - onDrillDown(data?.activeTooltipIndex ?? data?.index ?? 0) - } - /> - - ) : ( - - {/* Breadcrumb */} -
- -
+ return ( +
+ {/* Chart type toolbar */} +
+ {CHART_TYPE_BUTTONS.map(({ type, Icon, label }) => ( + + ))} +
- {/* Drill-down chart */} - {drillDownData && ( + {/* Main chart */} + + {!isDrillDown ? ( + + onDrillDown(data?.activeTooltipIndex ?? data?.index ?? 0) + } /> - )} - - {/* Back button */} - - - )} - -
- ); -}); + {/* Breadcrumb */} +
+ +
+ + {/* Drill-down chart */} + {drillDownData && ( + + )} + + {/* Back button */} + +
+ )} +
+
+ ); + }, +); InteractiveCharts.displayName = 'InteractiveCharts'; diff --git a/src/components/dashboard/RealTimeUpdater.tsx b/src/components/dashboard/RealTimeUpdater.tsx index 08a2cb8f..4bcd0dd7 100644 --- a/src/components/dashboard/RealTimeUpdater.tsx +++ b/src/components/dashboard/RealTimeUpdater.tsx @@ -32,20 +32,29 @@ const SPEED_OPTIONS = [ // ─── Component ──────────────────────────────────────────────────────────────── -export const RealTimeUpdater = React.memo(({ - title = 'Live Activity', - chartType = 'line', - websocketUrl, - updateInterval: initialInterval = 2000, - maxDataPoints = 20, - className = '', -}) => { - const [isPaused, setIsPaused] = useState(false); - const [interval, setIntervalValue] = useState(initialInterval); - const simulationEnabled = !websocketUrl; - - const { data, isConnected, isLoading, error, updateData, addDataPoint, config, calculateStats } = - useDataVisualization({ +export const RealTimeUpdater = React.memo( + ({ + title = 'Live Activity', + chartType = 'line', + websocketUrl, + updateInterval: initialInterval = 2000, + maxDataPoints = 20, + className = '', + }) => { + const [isPaused, setIsPaused] = useState(false); + const [interval, setIntervalValue] = useState(initialInterval); + const simulationEnabled = !websocketUrl; + + const { + data, + isConnected, + isLoading, + error, + updateData, + addDataPoint, + config, + calculateStats, + } = useDataVisualization({ initialData: { labels: [], datasets: [ @@ -68,187 +77,192 @@ export const RealTimeUpdater = React.memo(({ websocketUrl, }); - // Simulate real-time data when no WebSocket URL provided - useEffect(() => { - if (!simulationEnabled || isPaused) return; - - const timer = setInterval(() => { - const timeLabel = new Date().toLocaleTimeString(); - const value = Math.floor(Math.random() * 100); - addDataPoint(0, value, timeLabel); - - if (data && data.labels.length > maxDataPoints) { - updateData({ - labels: data.labels.slice(-maxDataPoints), - datasets: data.datasets.map((ds) => ({ - ...ds, - data: ds.data.slice(-maxDataPoints), - })), - }); - } - }, interval); - - return () => clearInterval(timer); - }, [simulationEnabled, isPaused, interval, maxDataPoints, addDataPoint, data, updateData]); - - const stats = calculateStats(); - - const statusText = isPaused - ? 'Paused' - : isConnected - ? 'Connected' - : simulationEnabled - ? 'Simulating' - : 'Disconnected'; - - const statusColor = isPaused - ? 'text-yellow-600 dark:text-yellow-400' - : isConnected || simulationEnabled - ? 'text-green-600 dark:text-green-400' - : 'text-red-600 dark:text-red-400'; - - const handleReset = useCallback(() => { - updateData({ - labels: [], - datasets: [ - { - label: 'Live Data', - data: [], - borderColor: '#3b82f6', - backgroundColor: 'rgba(59,130,246,0.1)', - borderWidth: 2, - }, - ], - }); - }, [updateData]); - - return ( -
- {/* Status bar */} -
-
- {/* Connection status */} -
- {(isConnected || simulationEnabled) && !isPaused ? ( -
+ // Simulate real-time data when no WebSocket URL provided + useEffect(() => { + if (!simulationEnabled || isPaused) return; + + const timer = setInterval(() => { + const timeLabel = new Date().toLocaleTimeString(); + const value = Math.floor(Math.random() * 100); + addDataPoint(0, value, timeLabel); + + if (data && data.labels.length > maxDataPoints) { + updateData({ + labels: data.labels.slice(-maxDataPoints), + datasets: data.datasets.map((ds) => ({ + ...ds, + data: ds.data.slice(-maxDataPoints), + })), + }); + } + }, interval); + + return () => clearInterval(timer); + }, [simulationEnabled, isPaused, interval, maxDataPoints, addDataPoint, data, updateData]); + + const stats = calculateStats(); + + const statusText = isPaused + ? 'Paused' + : isConnected + ? 'Connected' + : simulationEnabled + ? 'Simulating' + : 'Disconnected'; + + const statusColor = isPaused + ? 'text-yellow-600 dark:text-yellow-400' + : isConnected || simulationEnabled + ? 'text-green-600 dark:text-green-400' + : 'text-red-600 dark:text-red-400'; + + const handleReset = useCallback(() => { + updateData({ + labels: [], + datasets: [ + { + label: 'Live Data', + data: [], + borderColor: '#3b82f6', + backgroundColor: 'rgba(59,130,246,0.1)', + borderWidth: 2, + }, + ], + }); + }, [updateData]); - {/* Data point count */} - {data && data.labels.length > 0 && ( -
-