Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from 'next';
import { cookies } from 'next/headers';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
import { ThemeProvider } from '@/lib/theme-provider';
Expand Down Expand Up @@ -36,15 +37,40 @@ export default function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const cookieStore = cookies();
const themeCookie = cookieStore.get('theme');
const defaultTheme = themeCookie ? themeCookie.value : 'system';

const themeScript = `
(function() {
try {
var theme = document.cookie.match(/(?:^|; )theme=([^;]*)/);
var isDark = false;
if (theme && theme[1]) {
if (theme[1] === 'dark') isDark = true;
else if (theme[1] === 'system') isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
} else {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
if (isDark) {
document.documentElement.classList.add('dark');
}
} catch (e) {}
})();
`;

return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-50 transition-colors duration-200`}
>
<I18nProvider>
<InternationalizationEngine>
<CulturalAdaptationManager>
<ThemeProvider>
<ThemeProvider defaultTheme={defaultTheme}>
<DynamicTheming />
<AccessibilityProvider pageLabel="TeachLink — main application">
<PerformanceMonitoringProvider>
Expand Down
26 changes: 15 additions & 11 deletions src/components/dashboard/AdvancedDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

'use client';

import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import {
DndContext,
closestCenter,
Expand Down Expand Up @@ -41,7 +41,7 @@ interface SortablePanelProps {
children: React.ReactNode;
}

const SortablePanel: React.FC<SortablePanelProps> = ({ panel, children }) => {
const SortablePanel = React.memo<SortablePanelProps>(({ panel, children }) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: panel.id,
});
Expand All @@ -67,15 +67,17 @@ const SortablePanel: React.FC<SortablePanelProps> = ({ panel, children }) => {
{children}
</div>
);
};
});

SortablePanel.displayName = 'SortablePanel';

// ─── Main Component ───────────────────────────────────────────────────────────

export interface AdvancedDashboardProps {
className?: string;
}

export const AdvancedDashboard: React.FC<AdvancedDashboardProps> = ({ className = '' }) => {
export const AdvancedDashboard = React.memo<AdvancedDashboardProps>(({ className = '' }) => {
const {
panels,
filters,
Expand All @@ -101,7 +103,7 @@ export const AdvancedDashboard: React.FC<AdvancedDashboardProps> = ({ className
}),
);

const handleDragEnd = (event: DragEndEvent) => {
const handleDragEnd = useCallback((event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;

Expand All @@ -110,9 +112,9 @@ export const AdvancedDashboard: React.FC<AdvancedDashboardProps> = ({ 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);
Expand All @@ -122,16 +124,16 @@ export const AdvancedDashboard: React.FC<AdvancedDashboardProps> = ({ 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 (
<div className={`min-h-screen bg-gray-50 dark:bg-gray-900 p-4 sm:p-6 lg:p-8 ${className}`}>
Expand Down Expand Up @@ -243,4 +245,6 @@ export const AdvancedDashboard: React.FC<AdvancedDashboardProps> = ({ className
</DndContext>
</div>
);
};
});

AdvancedDashboard.displayName = 'AdvancedDashboard';
21 changes: 12 additions & 9 deletions src/components/dashboard/DashboardFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -45,7 +45,7 @@ const DEFAULT_METRICS = ['enrollments', 'revenue', 'completions', 'views'];

// ─── Component ────────────────────────────────────────────────────────────────

export const DashboardFilters: React.FC<DashboardFiltersProps> = ({
export const DashboardFilters = React.memo<DashboardFiltersProps>(({
filters,
onFiltersChange,
onReset,
Expand All @@ -55,22 +55,23 @@ export const DashboardFilters: React.FC<DashboardFiltersProps> = ({
}) => {
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 (
<div
Expand Down Expand Up @@ -249,4 +250,6 @@ export const DashboardFilters: React.FC<DashboardFiltersProps> = ({
)}
</div>
);
};
});

DashboardFilters.displayName = 'DashboardFilters';
6 changes: 4 additions & 2 deletions src/components/dashboard/InteractiveCharts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const CHART_TYPE_BUTTONS: { type: ChartType; Icon: React.ElementType; label: str

// ─── Component ────────────────────────────────────────────────────────────────

export const InteractiveCharts: React.FC<InteractiveChartsProps> = ({
export const InteractiveCharts = React.memo<InteractiveChartsProps>(({
panelId,
data,
chartType,
Expand Down Expand Up @@ -161,4 +161,6 @@ export const InteractiveCharts: React.FC<InteractiveChartsProps> = ({
</AnimatePresence>
</div>
);
};
});

InteractiveCharts.displayName = 'InteractiveCharts';
12 changes: 7 additions & 5 deletions src/components/dashboard/RealTimeUpdater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,7 +32,7 @@ const SPEED_OPTIONS = [

// ─── Component ────────────────────────────────────────────────────────────────

export const RealTimeUpdater: React.FC<RealTimeUpdaterProps> = ({
export const RealTimeUpdater = React.memo<RealTimeUpdaterProps>(({
title = 'Live Activity',
chartType = 'line',
websocketUrl,
Expand Down Expand Up @@ -107,7 +107,7 @@ export const RealTimeUpdater: React.FC<RealTimeUpdaterProps> = ({
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400';

const handleReset = () => {
const handleReset = useCallback(() => {
updateData({
labels: [],
datasets: [
Expand All @@ -120,7 +120,7 @@ export const RealTimeUpdater: React.FC<RealTimeUpdaterProps> = ({
},
],
});
};
}, [updateData]);

return (
<div className={`flex flex-col gap-4 ${className}`}>
Expand Down Expand Up @@ -249,4 +249,6 @@ export const RealTimeUpdater: React.FC<RealTimeUpdaterProps> = ({
)}
</div>
);
};
});

RealTimeUpdater.displayName = 'RealTimeUpdater';
9 changes: 7 additions & 2 deletions src/components/quizzes/QuestionCard.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -47,4 +48,8 @@ export default function QuestionCard({ question, quizState }: QuestionCardProps)
) : null}
</div>
);
}
});

QuestionCard.displayName = 'QuestionCard';

export default QuestionCard;
25 changes: 18 additions & 7 deletions src/components/quizzes/QuizContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import React, { useCallback } from 'react';
import QuestionCard from './QuestionCard';
import { Quiz, useQuiz } from '@/hooks/useQuiz';

Expand All @@ -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 {
Expand All @@ -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 (
<div className="max-w-4xl mx-auto p-6">
<div className="mb-6">
Expand Down Expand Up @@ -69,13 +76,13 @@ export default function QuizContainer({ quiz }: QuizContainerProps) {
</div>
<div className="mt-4 flex gap-3">
<button
onClick={() => actions.restart()}
onClick={handleRestart}
className="px-4 py-2 bg-gradient-to-r from-cyan-400 to-blue-500 text-white font-semibold rounded-lg hover:from-cyan-500 hover:to-blue-600 transition-all"
>
Try Again
</button>
<button
onClick={() => actions.setCurrentQuestionIndex(0)}
onClick={handleReviewAnswers}
className="px-4 py-2 text-[#0F172A] dark:text-white hover:text-[#0066FF] dark:hover:text-[#00C2FF] transition-colors"
>
Review Answers
Expand All @@ -88,7 +95,7 @@ export default function QuizContainer({ quiz }: QuizContainerProps) {

<div className="flex justify-between mt-6">
<button
onClick={() => actions.goPrevious()}
onClick={handleGoPrevious}
disabled={!quizState.canGoPrevious}
className="px-4 py-2 text-[#475569] dark:text-[#CBD5E1] hover:text-[#0066FF] dark:hover:text-[#00C2FF] disabled:opacity-50 transition-colors"
>
Expand All @@ -98,7 +105,7 @@ export default function QuizContainer({ quiz }: QuizContainerProps) {
<div className="flex items-center gap-3">
{quizState.canGoNext ? (
<button
onClick={() => actions.goNext()}
onClick={handleGoNext}
disabled={!quizState.canGoNext || isCompleted}
className="px-4 py-2 text-[#475569] dark:text-[#CBD5E1] hover:text-[#0066FF] dark:hover:text-[#00C2FF] disabled:opacity-50 transition-colors"
>
Expand All @@ -107,7 +114,7 @@ export default function QuizContainer({ quiz }: QuizContainerProps) {
) : null}

<button
onClick={() => actions.complete()}
onClick={handleComplete}
disabled={isCompleted}
className="px-4 py-2 bg-gradient-to-r from-cyan-400 to-blue-500 text-white font-semibold rounded-lg hover:from-cyan-500 hover:to-blue-600 transition-all disabled:opacity-50"
>
Expand All @@ -117,4 +124,8 @@ export default function QuizContainer({ quiz }: QuizContainerProps) {
</div>
</div>
);
}
});

QuizContainer.displayName = 'QuizContainer';

export default QuizContainer;
Loading
Loading