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
14 changes: 7 additions & 7 deletions src/app/search/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<main className="min-h-screen bg-slate-50/50">
<AdvancedSearchInterface />
</main>
);
return (
<main className="min-h-screen bg-slate-50/50">
<AdvancedSearchInterface />
</main>
);
}
4 changes: 1 addition & 3 deletions src/app/visualization-demo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,7 @@ export default function VisualizationDemoPage() {

{/* Footer */}
<div className="mt-12 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">
Features
</h3>
<h3 className="text-lg font-semibold mb-3 text-gray-900 dark:text-white">Features</h3>
<ul className="grid grid-cols-1 md:grid-cols-2 gap-3 text-gray-600 dark:text-gray-400">
<li className="flex items-start space-x-2">
<span className="text-green-500 mt-1">✓</span>
Expand Down
54 changes: 54 additions & 0 deletions src/components/mobile/AdaptiveLayouts.tsx
Original file line number Diff line number Diff line change
@@ -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<AdaptiveLayoutProps> = ({
mobileView,
desktopView,
breakpoint = 768,
}) => {
const [isMobile, setIsMobile] = useState<boolean>(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<typeof setTimeout>;
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 (
<div className={`flex flex-col md:flex-row w-full p-4 md:p-8 gap-4 md:gap-8 ${className}`}>
{children}
</div>
);
};
44 changes: 44 additions & 0 deletions src/components/mobile/GestureHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { HTMLAttributes } from 'react';
import { useMobileGestures } from '../../hooks/useMobileGestures';

interface GestureHandlerProps extends HTMLAttributes<HTMLDivElement> {
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
onSwipeUp?: () => void;
onSwipeDown?: () => void;
onPinchIn?: () => void;
onPinchOut?: () => void;
onTap?: () => void;
swipeThreshold?: number;
children: React.ReactNode;
}

export const GestureHandler: React.FC<GestureHandlerProps> = ({
onSwipeLeft,
onSwipeRight,
onSwipeUp,
onSwipeDown,
onPinchIn,
onPinchOut,
onTap,
swipeThreshold,
children,
...props
}) => {
const gestureProps = useMobileGestures({
onSwipeLeft,
onSwipeRight,
onSwipeUp,
onSwipeDown,
onPinchIn,
onPinchOut,
onTap,
swipeThreshold,
});

return (
<div {...gestureProps} {...props} style={{ touchAction: 'pan-y', ...props.style }}>
{children}
</div>
);
};
65 changes: 65 additions & 0 deletions src/components/mobile/MobileNavigation.tsx
Original file line number Diff line number Diff line change
@@ -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: <Home size={24} /> },
{ id: 'search', label: 'Search', icon: <Search size={24} /> },
{ id: 'courses', label: 'Courses', icon: <BookOpen size={24} /> },
{ id: 'profile', label: 'Profile', icon: <User size={24} /> },
];

const handleTabClick = (id: string) => {
setActiveTab(id);
if (onNavChange) onNavChange(id);
};

return (
<nav
className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 z-50 md:hidden"
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
>
<ul className="flex justify-around items-center h-16 px-2">
{navItems.map((item) => {
const isActive = activeTab === item.id;
return (
<li key={item.id} className="flex-1">
<button
onClick={() => handleTabClick(item.id)}
className={`w-full flex flex-col items-center justify-center py-2 space-y-1 transition-colors duration-200
${
isActive
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'
}
`}
aria-label={item.label}
>
<div
className={`${
isActive ? 'scale-110' : 'scale-100'
} transition-transform duration-200`}
>
{item.icon}
</div>
<span className="text-[10px] font-medium leading-none">{item.label}</span>
</button>
</li>
);
})}
</ul>
</nav>
);
};
122 changes: 122 additions & 0 deletions src/components/mobile/MobileOptimizedComponents.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement> {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
fullWidth?: boolean;
}

export const TouchButton: React.FC<TouchButtonProps> = ({
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 (
<button
className={`${baseClasses} ${sizeClasses} ${widthClasses} ${variantClasses[variant]} ${isTouched ? 'opacity-80' : 'opacity-100'} ${className}`}
onTouchStart={() => setIsTouched(true)}
onTouchEnd={() => setIsTouched(false)}
onTouchCancel={() => setIsTouched(false)}
{...props}
>
{children}
</button>
);
};

// Swipeable Card Component
interface SwipeableCardProps {
children: ReactNode;
onSwipeLeft?: () => void;
onSwipeRight?: () => void;
onSwipeUp?: () => void;
onSwipeDown?: () => void;
className?: string;
}

export const SwipeableCard: React.FC<SwipeableCardProps> = ({
children,
onSwipeLeft,
onSwipeRight,
onSwipeUp,
onSwipeDown,
className = '',
}) => {
return (
<GestureHandler
onSwipeLeft={onSwipeLeft}
onSwipeRight={onSwipeRight}
onSwipeUp={onSwipeUp}
onSwipeDown={onSwipeDown}
className={`bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm border border-gray-100 dark:border-gray-700 transition-transform active:scale-[0.98] ${className}`}
>
{children}
</GestureHandler>
);
};

// Bottom Sheet Modal (Mobile Optimized)
interface BottomSheetProps {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
title?: string;
}

export const BottomSheet: React.FC<BottomSheetProps> = ({
isOpen,
onClose,
children,
title,
}) => {
if (!isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex flex-col justify-end">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/40 backdrop-blur-sm transition-opacity"
onClick={onClose}
/>

{/* Sheet Content */}
<div className="relative bg-white dark:bg-gray-900 w-full rounded-t-3xl p-6 shadow-xl animate-in slide-in-from-bottom-full duration-300 pb-safe">
<GestureHandler onSwipeDown={onClose} swipeThreshold={40}>
{/* Handle bar for swiping */}
<div className="w-12 h-1.5 bg-gray-300 dark:bg-gray-600 rounded-full mx-auto mb-6 cursor-pointer" />
</GestureHandler>

{title && (
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
{title}
</h2>
)}

<div className="max-h-[70vh] overflow-y-auto">
{children}
</div>
</div>
</div>
);
};
Loading
Loading