diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx
index 4ce2ad8..1af4b69 100644
--- a/src/app/search/page.tsx
+++ b/src/app/search/page.tsx
@@ -1,14 +1,14 @@
import { AdvancedSearchInterface } from '@/components/search/AdvancedSearchInterface';
export const metadata = {
- title: 'Advanced Search | TeachLink',
- description: 'Powerful multi-dimensional search for the TeachLink ecosystem.',
+ title: 'Advanced Search | TeachLink',
+ description: 'Powerful multi-dimensional search for the TeachLink ecosystem.',
};
export default function SearchPage() {
- return (
-
-
-
- );
+ return (
+
+
+
+ );
}
diff --git a/src/app/visualization-demo/page.tsx b/src/app/visualization-demo/page.tsx
index 3c4e2e0..b774cbd 100644
--- a/src/app/visualization-demo/page.tsx
+++ b/src/app/visualization-demo/page.tsx
@@ -219,9 +219,7 @@ export default function VisualizationDemoPage() {
{/* Footer */}
-
- Features
-
+
Features
-
✓
diff --git a/src/components/mobile/AdaptiveLayouts.tsx b/src/components/mobile/AdaptiveLayouts.tsx
new file mode 100644
index 0000000..82e59e1
--- /dev/null
+++ b/src/components/mobile/AdaptiveLayouts.tsx
@@ -0,0 +1,54 @@
+import React, { ReactNode, useEffect, useState } from 'react';
+import { isMobileDevice } from '../../utils/mobileUtils';
+
+interface AdaptiveLayoutProps {
+ mobileView: ReactNode;
+ desktopView: ReactNode;
+ breakpoint?: number;
+}
+
+export const AdaptiveLayout: React.FC = ({
+ mobileView,
+ desktopView,
+ breakpoint = 768,
+}) => {
+ const [isMobile, setIsMobile] = useState(true); // Default to mobile for mobile-first approach
+
+ useEffect(() => {
+ const checkIsMobile = () => {
+ // Use both utility and explicit window width for precise responsive switching
+ const mobileByWidth = window.innerWidth <= breakpoint;
+ setIsMobile(mobileByWidth || isMobileDevice());
+ };
+
+ checkIsMobile(); // Initial check
+
+ // Performance optimization: debounce resize handler
+ let timeoutId: ReturnType;
+ const handleResize = () => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(checkIsMobile, 150);
+ };
+
+ window.addEventListener('resize', handleResize);
+ return () => {
+ window.removeEventListener('resize', handleResize);
+ clearTimeout(timeoutId);
+ };
+ }, [breakpoint]);
+
+ return <>{isMobile ? mobileView : desktopView}>;
+};
+
+// Also export a container that changes layout direction, padding, etc., based on sizing
+export const AdaptiveContainer: React.FC<{ children: React.ReactNode; className?: string }> = ({
+ children,
+ className = '',
+}) => {
+ // Mobile-first container: stacked by default, becomes flex-row on md screens
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/mobile/GestureHandler.tsx b/src/components/mobile/GestureHandler.tsx
new file mode 100644
index 0000000..809f9d8
--- /dev/null
+++ b/src/components/mobile/GestureHandler.tsx
@@ -0,0 +1,44 @@
+import React, { HTMLAttributes } from 'react';
+import { useMobileGestures } from '../../hooks/useMobileGestures';
+
+interface GestureHandlerProps extends HTMLAttributes {
+ onSwipeLeft?: () => void;
+ onSwipeRight?: () => void;
+ onSwipeUp?: () => void;
+ onSwipeDown?: () => void;
+ onPinchIn?: () => void;
+ onPinchOut?: () => void;
+ onTap?: () => void;
+ swipeThreshold?: number;
+ children: React.ReactNode;
+}
+
+export const GestureHandler: React.FC = ({
+ onSwipeLeft,
+ onSwipeRight,
+ onSwipeUp,
+ onSwipeDown,
+ onPinchIn,
+ onPinchOut,
+ onTap,
+ swipeThreshold,
+ children,
+ ...props
+}) => {
+ const gestureProps = useMobileGestures({
+ onSwipeLeft,
+ onSwipeRight,
+ onSwipeUp,
+ onSwipeDown,
+ onPinchIn,
+ onPinchOut,
+ onTap,
+ swipeThreshold,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/components/mobile/MobileNavigation.tsx b/src/components/mobile/MobileNavigation.tsx
new file mode 100644
index 0000000..112f464
--- /dev/null
+++ b/src/components/mobile/MobileNavigation.tsx
@@ -0,0 +1,65 @@
+import React, { useState } from 'react';
+import { Home, Search, BookOpen, User } from 'lucide-react';
+
+interface NavItem {
+ id: string;
+ label: string;
+ icon: React.ReactNode;
+ onClick?: () => void;
+}
+
+export const MobileNavigation: React.FC<{
+ initialActive?: string;
+ onNavChange?: (id: string) => void;
+}> = ({ initialActive = 'home', onNavChange }) => {
+ const [activeTab, setActiveTab] = useState(initialActive);
+
+ const navItems: NavItem[] = [
+ { id: 'home', label: 'Home', icon: },
+ { id: 'search', label: 'Search', icon: },
+ { id: 'courses', label: 'Courses', icon: },
+ { id: 'profile', label: 'Profile', icon: },
+ ];
+
+ const handleTabClick = (id: string) => {
+ setActiveTab(id);
+ if (onNavChange) onNavChange(id);
+ };
+
+ return (
+
+ );
+};
diff --git a/src/components/mobile/MobileOptimizedComponents.tsx b/src/components/mobile/MobileOptimizedComponents.tsx
new file mode 100644
index 0000000..9e19f15
--- /dev/null
+++ b/src/components/mobile/MobileOptimizedComponents.tsx
@@ -0,0 +1,122 @@
+import React, { ButtonHTMLAttributes, ReactNode, useState } from 'react';
+import { GestureHandler } from './GestureHandler';
+
+// Touch-Optimized Button with larger hit area and tap feedback
+interface TouchButtonProps extends ButtonHTMLAttributes {
+ children: ReactNode;
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
+ fullWidth?: boolean;
+}
+
+export const TouchButton: React.FC = ({
+ children,
+ variant = 'primary',
+ fullWidth = true,
+ className = '',
+ ...props
+}) => {
+ const [isTouched, setIsTouched] = useState(false);
+
+ const baseClasses =
+ 'relative overflow-hidden rounded-xl font-medium transition-all duration-200 active:scale-95 flex items-center justify-center';
+ const sizeClasses = 'min-h-[48px] px-6 py-3 text-base'; // Minimum 48px height for touch targets
+ const widthClasses = fullWidth ? 'w-full' : '';
+
+ const variantClasses = {
+ primary: 'bg-blue-600 text-white hover:bg-blue-700 active:bg-blue-800',
+ secondary:
+ 'bg-gray-200 text-gray-900 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-700 active:bg-gray-300 dark:active:bg-gray-600',
+ outline:
+ 'border-2 border-blue-600 text-blue-600 dark:border-blue-400 dark:text-blue-400 bg-transparent active:bg-blue-50 dark:active:bg-gray-800',
+ ghost:
+ 'bg-transparent text-gray-700 dark:text-gray-300 active:bg-gray-100 dark:active:bg-gray-800',
+ };
+
+ return (
+
+ );
+};
+
+// Swipeable Card Component
+interface SwipeableCardProps {
+ children: ReactNode;
+ onSwipeLeft?: () => void;
+ onSwipeRight?: () => void;
+ onSwipeUp?: () => void;
+ onSwipeDown?: () => void;
+ className?: string;
+}
+
+export const SwipeableCard: React.FC = ({
+ children,
+ onSwipeLeft,
+ onSwipeRight,
+ onSwipeUp,
+ onSwipeDown,
+ className = '',
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+// Bottom Sheet Modal (Mobile Optimized)
+interface BottomSheetProps {
+ isOpen: boolean;
+ onClose: () => void;
+ children: ReactNode;
+ title?: string;
+}
+
+export const BottomSheet: React.FC = ({
+ isOpen,
+ onClose,
+ children,
+ title,
+}) => {
+ if (!isOpen) return null;
+
+ return (
+
+ {/* Backdrop */}
+
+
+ {/* Sheet Content */}
+
+
+ {/* Handle bar for swiping */}
+
+
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+ {children}
+
+
+
+ );
+};
diff --git a/src/hooks/useMobileGestures.tsx b/src/hooks/useMobileGestures.tsx
new file mode 100644
index 0000000..52cc07b
--- /dev/null
+++ b/src/hooks/useMobileGestures.tsx
@@ -0,0 +1,78 @@
+import { useState, TouchEvent, useCallback } from 'react';
+import { calculateSwipeDirection, calculateDistance } from '../utils/mobileUtils';
+
+interface GestureHandlers {
+ onSwipeLeft?: () => void;
+ onSwipeRight?: () => void;
+ onSwipeUp?: () => void;
+ onSwipeDown?: () => void;
+ onPinchIn?: () => void;
+ onPinchOut?: () => void;
+ onTap?: () => void;
+ swipeThreshold?: number;
+}
+
+export const useMobileGestures = (handlers: GestureHandlers) => {
+ const [touchStart, setTouchStart] = useState<{ x: number; y: number } | null>(null);
+ const [initialPinchDistance, setInitialPinchDistance] = useState(null);
+
+ const handleTouchStart = useCallback((e: TouchEvent) => {
+ if (e.touches.length === 1) {
+ setTouchStart({ x: e.touches[0].clientX, y: e.touches[0].clientY });
+ } else if (e.touches.length === 2) {
+ const dist = calculateDistance(
+ e.touches[0].clientX, e.touches[0].clientY,
+ e.touches[1].clientX, e.touches[1].clientY
+ );
+ setInitialPinchDistance(dist);
+ }
+ }, []);
+
+ const handleTouchEnd = useCallback((e: TouchEvent) => {
+ if (e.changedTouches.length === 1 && touchStart) {
+ const touchEnd = { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY };
+ const isTap = Math.abs(touchEnd.x - touchStart.x) < 10 && Math.abs(touchEnd.y - touchStart.y) < 10;
+
+ if (isTap && handlers.onTap) {
+ handlers.onTap();
+ } else {
+ const direction = calculateSwipeDirection(
+ touchStart.x, touchStart.y,
+ touchEnd.x, touchEnd.y,
+ handlers.swipeThreshold || 50
+ );
+
+ if (direction === 'LEFT' && handlers.onSwipeLeft) handlers.onSwipeLeft();
+ if (direction === 'RIGHT' && handlers.onSwipeRight) handlers.onSwipeRight();
+ if (direction === 'UP' && handlers.onSwipeUp) handlers.onSwipeUp();
+ if (direction === 'DOWN' && handlers.onSwipeDown) handlers.onSwipeDown();
+ }
+ }
+ setTouchStart(null);
+ setInitialPinchDistance(null);
+ }, [touchStart, handlers]);
+
+ const handleTouchMove = useCallback((e: TouchEvent) => {
+ if (e.touches.length === 2 && initialPinchDistance !== null) {
+ const currentDistance = calculateDistance(
+ e.touches[0].clientX, e.touches[0].clientY,
+ e.touches[1].clientX, e.touches[1].clientY
+ );
+
+ const pinchThreshold = 20;
+ if (currentDistance - initialPinchDistance > pinchThreshold && handlers.onPinchOut) {
+ handlers.onPinchOut();
+ setInitialPinchDistance(currentDistance); // Reset to detect continuous pinch
+ } else if (initialPinchDistance - currentDistance > pinchThreshold && handlers.onPinchIn) {
+ handlers.onPinchIn();
+ setInitialPinchDistance(currentDistance);
+ }
+ }
+ }, [initialPinchDistance, handlers]);
+
+ return {
+ onTouchStart: handleTouchStart,
+ onTouchMove: handleTouchMove,
+ onTouchEnd: handleTouchEnd,
+ };
+};
diff --git a/src/utils/mobileUtils.ts b/src/utils/mobileUtils.ts
new file mode 100644
index 0000000..1e71d04
--- /dev/null
+++ b/src/utils/mobileUtils.ts
@@ -0,0 +1,32 @@
+export const isMobileDevice = (): boolean => {
+ if (typeof window === 'undefined') return false;
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || window.innerWidth <= 768;
+};
+
+export const calculateSwipeDirection = (
+ startX: number,
+ startY: number,
+ endX: number,
+ endY: number,
+ threshold = 50
+): 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' | null => {
+ const diffX = endX - startX;
+ const diffY = endY - startY;
+
+ if (Math.abs(diffX) > Math.abs(diffY)) {
+ // Horizontal swipe
+ if (Math.abs(diffX) > threshold) {
+ return diffX > 0 ? 'RIGHT' : 'LEFT';
+ }
+ } else {
+ // Vertical swipe
+ if (Math.abs(diffY) > threshold) {
+ return diffY > 0 ? 'DOWN' : 'UP';
+ }
+ }
+ return null;
+};
+
+export const calculateDistance = (x1: number, y1: number, x2: number, y2: number): number => {
+ return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+};