From e2c749bb460abf7fd4a144fdfa554966b5ca63ed Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Wed, 25 Mar 2026 14:41:03 -0700 Subject: [PATCH 1/2] Implement Advanced Mobile Responsive Components --- src/components/mobile/AdaptiveLayouts.tsx | 54 ++++++++ src/components/mobile/GestureHandler.tsx | 31 +++++ src/components/mobile/MobileNavigation.tsx | 54 ++++++++ .../mobile/MobileOptimizedComponents.tsx | 121 ++++++++++++++++++ src/hooks/useMobileGestures.tsx | 78 +++++++++++ src/utils/mobileUtils.ts | 32 +++++ 6 files changed, 370 insertions(+) create mode 100644 src/components/mobile/AdaptiveLayouts.tsx create mode 100644 src/components/mobile/GestureHandler.tsx create mode 100644 src/components/mobile/MobileNavigation.tsx create mode 100644 src/components/mobile/MobileOptimizedComponents.tsx create mode 100644 src/hooks/useMobileGestures.tsx create mode 100644 src/utils/mobileUtils.ts diff --git a/src/components/mobile/AdaptiveLayouts.tsx b/src/components/mobile/AdaptiveLayouts.tsx new file mode 100644 index 0000000..c6c4295 --- /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..ea0dec8 --- /dev/null +++ b/src/components/mobile/GestureHandler.tsx @@ -0,0 +1,31 @@ +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..1acf7d6 --- /dev/null +++ b/src/components/mobile/MobileNavigation.tsx @@ -0,0 +1,54 @@ +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..df32158 --- /dev/null +++ b/src/components/mobile/MobileOptimizedComponents.tsx @@ -0,0 +1,121 @@ +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)); +}; From 8cc674c896a0ebbcb884ed15d7374a2b536564b9 Mon Sep 17 00:00:00 2001 From: gabito1451 Date: Wed, 25 Mar 2026 19:48:56 -0700 Subject: [PATCH 2/2] fix --- src/app/search/page.tsx | 14 +++---- src/app/visualization-demo/page.tsx | 4 +- src/components/mobile/AdaptiveLayouts.tsx | 14 +++---- src/components/mobile/GestureHandler.tsx | 23 +++++++++--- src/components/mobile/MobileNavigation.tsx | 17 +++++++-- .../mobile/MobileOptimizedComponents.tsx | 37 ++++++++++--------- 6 files changed, 66 insertions(+), 43 deletions(-) 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 index c6c4295..82e59e1 100644 --- a/src/components/mobile/AdaptiveLayouts.tsx +++ b/src/components/mobile/AdaptiveLayouts.tsx @@ -7,10 +7,10 @@ interface AdaptiveLayoutProps { breakpoint?: number; } -export const AdaptiveLayout: React.FC = ({ - mobileView, - desktopView, - breakpoint = 768 +export const AdaptiveLayout: React.FC = ({ + mobileView, + desktopView, + breakpoint = 768, }) => { const [isMobile, setIsMobile] = useState(true); // Default to mobile for mobile-first approach @@ -22,7 +22,7 @@ export const AdaptiveLayout: React.FC = ({ }; checkIsMobile(); // Initial check - + // Performance optimization: debounce resize handler let timeoutId: ReturnType; const handleResize = () => { @@ -41,9 +41,9 @@ export const AdaptiveLayout: React.FC = ({ }; // Also export a container that changes layout direction, padding, etc., based on sizing -export const AdaptiveContainer: React.FC<{children: React.ReactNode; className?: string}> = ({ +export const AdaptiveContainer: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, - className = '' + className = '', }) => { // Mobile-first container: stacked by default, becomes flex-row on md screens return ( diff --git a/src/components/mobile/GestureHandler.tsx b/src/components/mobile/GestureHandler.tsx index ea0dec8..809f9d8 100644 --- a/src/components/mobile/GestureHandler.tsx +++ b/src/components/mobile/GestureHandler.tsx @@ -14,13 +14,26 @@ interface GestureHandlerProps extends HTMLAttributes { } export const GestureHandler: React.FC = ({ - onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, - onPinchIn, onPinchOut, onTap, swipeThreshold, - children, ...props + onSwipeLeft, + onSwipeRight, + onSwipeUp, + onSwipeDown, + onPinchIn, + onPinchOut, + onTap, + swipeThreshold, + children, + ...props }) => { const gestureProps = useMobileGestures({ - onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, - onPinchIn, onPinchOut, onTap, swipeThreshold + onSwipeLeft, + onSwipeRight, + onSwipeUp, + onSwipeDown, + onPinchIn, + onPinchOut, + onTap, + swipeThreshold, }); return ( diff --git a/src/components/mobile/MobileNavigation.tsx b/src/components/mobile/MobileNavigation.tsx index 1acf7d6..112f464 100644 --- a/src/components/mobile/MobileNavigation.tsx +++ b/src/components/mobile/MobileNavigation.tsx @@ -27,7 +27,10 @@ export const MobileNavigation: React.FC<{ }; return ( -