diff --git a/src/components/dashboard/AdvancedDashboard.tsx b/src/components/dashboard/AdvancedDashboard.tsx index 10291988..ee929a06 100644 --- a/src/components/dashboard/AdvancedDashboard.tsx +++ b/src/components/dashboard/AdvancedDashboard.tsx @@ -7,7 +7,7 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { DndContext, closestCenter, @@ -41,7 +41,7 @@ interface SortablePanelProps { children: React.ReactNode; } -const SortablePanel: React.FC = ({ panel, children }) => { +const SortablePanel = React.memo(({ panel, children }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: panel.id, }); @@ -67,7 +67,9 @@ const SortablePanel: React.FC = ({ panel, children }) => { {children} ); -}; +}); + +SortablePanel.displayName = 'SortablePanel'; // ─── Main Component ─────────────────────────────────────────────────────────── @@ -75,7 +77,7 @@ export interface AdvancedDashboardProps { className?: string; } -export const AdvancedDashboard: React.FC = ({ className = '' }) => { +export const AdvancedDashboard = React.memo(({ className = '' }) => { const { panels, filters, @@ -101,7 +103,7 @@ export const AdvancedDashboard: React.FC = ({ className }), ); - const handleDragEnd = (event: DragEndEvent) => { + const handleDragEnd = useCallback((event: DragEndEvent) => { const { active, over } = event; if (!over || active.id === over.id) return; @@ -110,9 +112,9 @@ export const AdvancedDashboard: React.FC = ({ className if (fromIndex !== -1 && toIndex !== -1) { reorderPanels(fromIndex, toIndex); } - }; + }, [panels, reorderPanels]); - const handleShare = async () => { + const handleShare = useCallback(async () => { const url = generateShareURL(); try { await navigator.clipboard.writeText(url); @@ -122,16 +124,16 @@ export const AdvancedDashboard: React.FC = ({ className } catch { toast.error('Could not copy to clipboard'); } - }; + }, [generateShareURL]); - const handleExportAll = () => { + const handleExportAll = useCallback(() => { panels.forEach((panel) => { if (panel.id !== 'realtime') { exportPanel(panel.id, 'csv'); } }); toast.success('Panels exported as CSV'); - }; + }, [panels, exportPanel]); return (
@@ -243,4 +245,6 @@ export const AdvancedDashboard: React.FC = ({ className
); -}; +}); + +AdvancedDashboard.displayName = 'AdvancedDashboard'; diff --git a/src/components/dashboard/DashboardFilters.tsx b/src/components/dashboard/DashboardFilters.tsx index 05efe818..21c5ca86 100644 --- a/src/components/dashboard/DashboardFilters.tsx +++ b/src/components/dashboard/DashboardFilters.tsx @@ -6,7 +6,7 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import { Filter, X, RotateCcw } from 'lucide-react'; import { TimeRange, AggregationType, CHART_COLOR_PALETTE } from '@/utils/visualizationUtils'; import type { DashboardFiltersState } from '@/hooks/useDashboardData'; @@ -45,7 +45,7 @@ const DEFAULT_METRICS = ['enrollments', 'revenue', 'completions', 'views']; // ─── Component ──────────────────────────────────────────────────────────────── -export const DashboardFilters: React.FC = ({ +export const DashboardFilters = React.memo(({ filters, onFiltersChange, onReset, @@ -55,22 +55,23 @@ export const DashboardFilters: React.FC = ({ }) => { const [isOpen, setIsOpen] = useState(false); - const toggleCategory = (cat: string) => { + 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 = (cat: string) => { + const removeCategory = useCallback((cat: string) => { onFiltersChange({ categories: filters.categories.filter((c) => c !== cat) }); - }; + }, [filters.categories, onFiltersChange]); - const activeFilterCount = + const activeFilterCount = useMemo(() => ( (filters.timeRange !== '30d' ? 1 : 0) + filters.categories.length + (filters.metric !== 'enrollments' ? 1 : 0) + - (filters.aggregation !== 'sum' ? 1 : 0); + (filters.aggregation !== 'sum' ? 1 : 0) + ), [filters]); return (
= ({ )}
); -}; +}); + +DashboardFilters.displayName = 'DashboardFilters'; diff --git a/src/components/dashboard/InteractiveCharts.tsx b/src/components/dashboard/InteractiveCharts.tsx index a3de35b0..b79548fa 100644 --- a/src/components/dashboard/InteractiveCharts.tsx +++ b/src/components/dashboard/InteractiveCharts.tsx @@ -49,7 +49,7 @@ const CHART_TYPE_BUTTONS: { type: ChartType; Icon: React.ElementType; label: str // ─── Component ──────────────────────────────────────────────────────────────── -export const InteractiveCharts: React.FC = ({ +export const InteractiveCharts = React.memo(({ panelId, data, chartType, @@ -161,4 +161,6 @@ export const InteractiveCharts: React.FC = ({ ); -}; +}); + +InteractiveCharts.displayName = 'InteractiveCharts'; diff --git a/src/components/dashboard/RealTimeUpdater.tsx b/src/components/dashboard/RealTimeUpdater.tsx index dd550f48..08a2cb8f 100644 --- a/src/components/dashboard/RealTimeUpdater.tsx +++ b/src/components/dashboard/RealTimeUpdater.tsx @@ -6,7 +6,7 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import { Wifi, WifiOff, Activity, Pause, Play, RefreshCw } from 'lucide-react'; import { InteractiveChartLibrary } from '@/components/visualization/InteractiveChartLibrary'; import { useDataVisualization } from '@/hooks/useDataVisualization'; @@ -32,7 +32,7 @@ const SPEED_OPTIONS = [ // ─── Component ──────────────────────────────────────────────────────────────── -export const RealTimeUpdater: React.FC = ({ +export const RealTimeUpdater = React.memo(({ title = 'Live Activity', chartType = 'line', websocketUrl, @@ -107,7 +107,7 @@ export const RealTimeUpdater: React.FC = ({ ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'; - const handleReset = () => { + const handleReset = useCallback(() => { updateData({ labels: [], datasets: [ @@ -120,7 +120,7 @@ export const RealTimeUpdater: React.FC = ({ }, ], }); - }; + }, [updateData]); return (
@@ -249,4 +249,6 @@ export const RealTimeUpdater: React.FC = ({ )}
); -}; +}); + +RealTimeUpdater.displayName = 'RealTimeUpdater'; diff --git a/src/components/quizzes/QuestionCard.tsx b/src/components/quizzes/QuestionCard.tsx index 71dc6ba8..ad8fb65d 100644 --- a/src/components/quizzes/QuestionCard.tsx +++ b/src/components/quizzes/QuestionCard.tsx @@ -1,5 +1,6 @@ 'use client'; +import React from 'react'; import MultipleChoiceQuestion from './question-types/MultipleChoiceQuestion'; import TrueFalseQuestion from './question-types/TrueFalseQuestion'; import CodeChallengeQuestion from './question-types/CodeChallengeQuestion'; @@ -10,7 +11,7 @@ interface QuestionCardProps { quizState: UseQuizReturn; } -export default function QuestionCard({ question, quizState }: QuestionCardProps) { +const QuestionCard = React.memo(({ question, quizState }: QuestionCardProps) => { const answer = quizState.answers[question.id]; const showFeedback = Boolean(answer?.feedback); @@ -47,4 +48,8 @@ export default function QuestionCard({ question, quizState }: QuestionCardProps) ) : null} ); -} +}); + +QuestionCard.displayName = 'QuestionCard'; + +export default QuestionCard; diff --git a/src/components/quizzes/QuizContainer.tsx b/src/components/quizzes/QuizContainer.tsx index 0482b1d0..ec68de0e 100644 --- a/src/components/quizzes/QuizContainer.tsx +++ b/src/components/quizzes/QuizContainer.tsx @@ -1,5 +1,6 @@ 'use client'; +import React, { useCallback } from 'react'; import QuestionCard from './QuestionCard'; import { Quiz, useQuiz } from '@/hooks/useQuiz'; @@ -13,7 +14,7 @@ function formatTime(totalSeconds: number) { return `${minutes}:${seconds.toString().padStart(2, '0')}`; } -export default function QuizContainer({ quiz }: QuizContainerProps) { +const QuizContainer = React.memo(({ quiz }: QuizContainerProps) => { const quizState = useQuiz({ quiz, autoStart: true }); const { @@ -30,6 +31,12 @@ export default function QuizContainer({ quiz }: QuizContainerProps) { const totalQuestions = quiz.questions.length; + const handleRestart = useCallback(() => actions.restart(), [actions]); + const handleReviewAnswers = useCallback(() => actions.setCurrentQuestionIndex(0), [actions]); + const handleGoPrevious = useCallback(() => actions.goPrevious(), [actions]); + const handleGoNext = useCallback(() => actions.goNext(), [actions]); + const handleComplete = useCallback(() => actions.complete(), [actions]); + return (
@@ -69,13 +76,13 @@ export default function QuizContainer({ quiz }: QuizContainerProps) {
); -} +}); + +QuizContainer.displayName = 'QuizContainer'; + +export default QuizContainer; diff --git a/src/components/search/AdvancedSearchInterface.tsx b/src/components/search/AdvancedSearchInterface.tsx index 93b2fd5d..28c58bfa 100644 --- a/src/components/search/AdvancedSearchInterface.tsx +++ b/src/components/search/AdvancedSearchInterface.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { Search, Filter, @@ -17,7 +17,7 @@ import { IntelligentAutoComplete } from './IntelligentAutoComplete'; import { FacetedFilterSystem } from './FacetedFilterSystem'; import { SearchResultsVisualizer } from './SearchResultsVisualizer'; -export const AdvancedSearchInterface: React.FC = () => { +export const AdvancedSearchInterface = React.memo(() => { const { query, updateSearchText, @@ -33,17 +33,17 @@ export const AdvancedSearchInterface: React.FC = () => { const [showFilters, setShowFilters] = useState(false); const [hasSearched, setHasSearched] = useState(false); - const handleSearch = (text: string) => { + const handleSearch = useCallback((text: string) => { updateSearchText(text); performSearch(); setHasSearched(true); - }; + }, [updateSearchText, performSearch]); - const handleReset = () => { + const handleReset = useCallback(() => { clearFilters(); updateSearchText(''); setHasSearched(false); - }; + }, [clearFilters, updateSearchText]); return (
@@ -205,6 +205,8 @@ export const AdvancedSearchInterface: React.FC = () => {
); -}; +}); + +AdvancedSearchInterface.displayName = 'AdvancedSearchInterface'; export default AdvancedSearchInterface; diff --git a/src/components/search/FacetedFilterSystem.tsx b/src/components/search/FacetedFilterSystem.tsx index 7294d8f0..a57a3d55 100644 --- a/src/components/search/FacetedFilterSystem.tsx +++ b/src/components/search/FacetedFilterSystem.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useCallback } from 'react'; import { BarChart, DollarSign, @@ -47,12 +47,12 @@ const DIFFICULTY_LEVELS = [ { id: 'advanced', label: 'Advanced', color: 'bg-red-500' }, ]; -export const FacetedFilterSystem: React.FC = ({ +export const FacetedFilterSystem = React.memo(({ filters, onFilterChange, onReset, }) => { - const toggleContentType = (type: SearchContentType) => { + const toggleContentType = useCallback((type: SearchContentType) => { let newTypes: SearchContentType[]; if (type === 'all') { newTypes = ['all']; @@ -66,13 +66,13 @@ export const FacetedFilterSystem: React.FC = ({ } } onFilterChange({ types: newTypes }); - }; + }, [filters.types, onFilterChange]); - const toggleDifficulty = (level: string) => { + const toggleDifficulty = useCallback((level: string) => { const current = filters.difficulty; const next = current.includes(level) ? current.filter((l) => l !== level) : [...current, level]; onFilterChange({ difficulty: next }); - }; + }, [filters.difficulty, onFilterChange]); return (
@@ -217,4 +217,6 @@ export const FacetedFilterSystem: React.FC = ({
); -}; +}); + +FacetedFilterSystem.displayName = 'FacetedFilterSystem'; diff --git a/src/components/search/FilterSidebar.tsx b/src/components/search/FilterSidebar.tsx index 70714391..6ff54938 100644 --- a/src/components/search/FilterSidebar.tsx +++ b/src/components/search/FilterSidebar.tsx @@ -1,6 +1,6 @@ 'use client'; -import React from 'react'; +import React, { useCallback } from 'react'; import { BarChart2, Clock, Tag, Users, Search, RotateCcw } from 'lucide-react'; import { FilterState } from '../../hooks/useSearchFilters'; import { RangeSlider } from '../ui/RangeSlider'; @@ -14,26 +14,26 @@ interface FilterSidebarProps { const TOPIC_OPTIONS = ['Design', 'Coding', 'Business', 'Marketing', 'Health']; -export const FilterSidebar: React.FC = ({ +export const FilterSidebar = React.memo(({ filters, onFilterChange, onReset, }) => { - const handleDifficultyChange = (input: string) => { + const handleDifficultyChange = useCallback((input: string) => { const current = filters.difficulty || []; const updated = current.includes(input) ? current.filter((d: string) => d !== input) : [...current, input]; onFilterChange({ difficulty: updated }); - }; + }, [filters.difficulty, onFilterChange]); - const handleInstructorChange = (name: string) => { + const handleInstructorChange = useCallback((name: string) => { if (filters.instructor === name) { onFilterChange({ instructor: '' }); } else { onFilterChange({ instructor: name }); } - }; + }, [filters.instructor, onFilterChange]); return ( ); -}; +}); + +FilterSidebar.displayName = 'FilterSidebar'; diff --git a/src/components/search/IntelligentAutoComplete.tsx b/src/components/search/IntelligentAutoComplete.tsx index 67f69ed6..643e620b 100644 --- a/src/components/search/IntelligentAutoComplete.tsx +++ b/src/components/search/IntelligentAutoComplete.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Search, X, Clock, ChevronRight, Sparkles, User, Tag, Type } from 'lucide-react'; import { getSearchSuggestions, highlightMatch } from '../../utils/searchUtils'; @@ -11,7 +11,7 @@ interface IntelligentAutoCompleteProps { history?: string[]; } -export const IntelligentAutoComplete: React.FC = ({ +export const IntelligentAutoComplete = React.memo(({ value, onChange, onSearch, @@ -33,7 +33,14 @@ export const IntelligentAutoComplete: React.FC = ( return () => document.removeEventListener('mousedown', handleClickOutside); }, []); - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleSelect = useCallback((suggestion: string) => { + onChange(suggestion); + onSearch(suggestion); + setIsOpen(false); + setActiveIndex(-1); + }, [onChange, onSearch]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'ArrowDown') { setActiveIndex((prev) => Math.min(prev + 1, suggestions.length - 1)); e.preventDefault(); @@ -50,14 +57,9 @@ export const IntelligentAutoComplete: React.FC = ( } else if (e.key === 'Escape') { setIsOpen(false); } - }; + }, [activeIndex, handleSelect, onSearch, suggestions, value]); + - const handleSelect = (suggestion: string) => { - onChange(suggestion); - onSearch(suggestion); - setIsOpen(false); - setActiveIndex(-1); - }; const getSuggestionIcon = (suggestion: string) => { if (suggestion.startsWith('author:')) return ; @@ -195,4 +197,6 @@ export const IntelligentAutoComplete: React.FC = ( )} ); -}; +}); + +IntelligentAutoComplete.displayName = 'IntelligentAutoComplete'; diff --git a/src/components/search/SearchFilters.tsx b/src/components/search/SearchFilters.tsx index cd1fa400..f87b2b58 100644 --- a/src/components/search/SearchFilters.tsx +++ b/src/components/search/SearchFilters.tsx @@ -10,10 +10,12 @@ interface SearchFiltersProps { resetFilters: () => void; } -export const SearchFilters: React.FC = ({ +export const SearchFilters = React.memo(({ filters, setFilters, resetFilters, }) => { return ; -}; +}); + +SearchFilters.displayName = 'SearchFilters'; diff --git a/src/components/search/SearchResultsVisualizer.tsx b/src/components/search/SearchResultsVisualizer.tsx index 89e848b0..17a31858 100644 --- a/src/components/search/SearchResultsVisualizer.tsx +++ b/src/components/search/SearchResultsVisualizer.tsx @@ -22,7 +22,7 @@ interface SearchResultsVisualizerProps { onSortChange: (sortBy: any) => void; } -export const SearchResultsVisualizer: React.FC = ({ +export const SearchResultsVisualizer = React.memo(({ results, isSearching, sortBy, @@ -167,9 +167,11 @@ export const SearchResultsVisualizer: React.FC = ( ); -}; +}); -const BarChart = ({ className }: { className?: string }) => ( +SearchResultsVisualizer.displayName = 'SearchResultsVisualizer'; + +const BarChart = React.memo(({ className }: { className?: string }) => ( ( -); +)); + +BarChart.displayName = 'BarChart';