import React, { useState, useMemo, useEffect, useRef } from 'react'; import { Search, X, ArrowRight, Compass, Ship, Utensils, Anchor, Film } from 'lucide-react'; import { useLanguage } from '../contexts/LanguageContext.tsx'; import { useTheme } from '../contexts/ThemeContext.tsx'; import { CONTENT } from '../constants.ts'; import { useNavigate } from 'react-router-dom'; interface SearchOverlayProps { isOpen: boolean; onClose: () => void; } interface SearchResult { title: string; subtitle?: string; category: 'home' | 'itinerary' | 'ships' | 'dining' | 'activity'; link: string; matchType: 'title' | 'content'; } interface SearchSuggestion { text: string; count: number; category: 'home' | 'itinerary' | 'ships' | 'dining' | 'activity' | 'all'; } const SearchOverlay: React.FC = ({ isOpen, onClose }) => { const { language } = useLanguage(); const { itineraries, ships, dining, videoSection } = useTheme(); const [query, setQuery] = useState(''); const [showSuggestions, setShowSuggestions] = useState(true); const inputRef = useRef(null); const navigate = useNavigate(); const t = CONTENT[language]; const searchT = (CONTENT[language] as any).search || { placeholder: "Search...", no_results: "No results", results_for: "Results: ", categories: { home: "Home", ships: "Ships", itinerary: "Trips", dining: "Food", activity: "Fun" } }; // Focus input when opened useEffect(() => { if (isOpen) { setTimeout(() => { inputRef.current?.focus(); }, 100); // Removed body scroll lock to keep it feeling lightweight } }, [isOpen]); // Handle ESC key useEffect(() => { const handleEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleEsc); return () => window.removeEventListener('keydown', handleEsc); }, [onClose]); // Build Search Index const searchIndex: SearchResult[] = useMemo(() => { const index: SearchResult[] = []; // 1. Home - Hero (Text on Image) index.push({ title: t.hero.title_normal + ' ' + t.hero.title_italic, subtitle: t.hero.subtitle, category: 'home', link: '/', matchType: 'title' }); // 2. Dynamic Itineraries (Cards / Images) itineraries.forEach(item => { index.push({ title: item.title, subtitle: `${item.route} - ${item.price}`, category: 'itinerary', link: '/?section=featured-itineraries', matchType: 'title' }); }); // 3. Dynamic Ships (Cards / Images) ships.forEach(item => { index.push({ title: item.name, subtitle: item.description, category: 'ships', link: '/?section=our-fleet', matchType: 'title' }); }); // 4. Dynamic Dining (Banner Image) index.push({ title: dining.title, subtitle: dining.description, category: 'dining', link: '/?section=dining-experience', matchType: 'title' }); // 5. Dynamic Video/Activity (Thumbnail) index.push({ title: videoSection.title + ' ' + videoSection.titleItalic, subtitle: videoSection.description, category: 'activity', link: '/?section=video-experience', matchType: 'title' }); // 6. Ships Page Content (Text on Images: Hero & Cards) const shipPage = (t as any).shipsPage; if (shipPage) { // Hero index.push({ title: shipPage.hero_sub, subtitle: shipPage.hero_desc, category: 'ships', link: '/ships', matchType: 'title' }); // Lanyue Image Card index.push({ title: shipPage.lanyue.title, subtitle: shipPage.lanyue.desc, category: 'ships', link: '/ships?section=lanyue', matchType: 'title' }); // Aurora Image Card index.push({ title: shipPage.aurora.title, subtitle: shipPage.aurora.desc, category: 'ships', link: '/ships?section=aurora', matchType: 'title' }); // Intro Text index.push({ title: shipPage.intro.title, subtitle: shipPage.intro.desc, category: 'ships', link: '/ships?section=series', matchType: 'content' }); } // 7. CruiseSpace Page Content const cruiseSpacePage = (t as any).shipsPage?.spaces; if (cruiseSpacePage) { // Facilities index.push({ title: cruiseSpacePage.facilities.title, subtitle: cruiseSpacePage.facilities.desc, category: 'ships', link: '/cruise-space?section=facilities', matchType: 'title' }); // Facilities Items cruiseSpacePage.facilities.items?.forEach((item: any) => { index.push({ title: item.title, subtitle: item.desc, category: 'ships', link: '/cruise-space?section=facilities', matchType: 'content' }); }); // Rooms index.push({ title: cruiseSpacePage.rooms.title, subtitle: cruiseSpacePage.rooms.desc, category: 'ships', link: '/cruise-space?section=rooms', matchType: 'title' }); // Rooms Types cruiseSpacePage.rooms.types?.forEach((room: any) => { index.push({ title: room.name, subtitle: room.desc, category: 'ships', link: '/cruise-space?section=rooms', matchType: 'content' }); }); // VIP Privileges index.push({ title: cruiseSpacePage.vip.title, subtitle: cruiseSpacePage.vip.desc, category: 'ships', link: '/cruise-space?section=vip', matchType: 'title' }); // VIP Benefits cruiseSpacePage.vip.benefits?.forEach((benefit: string) => { index.push({ title: benefit, category: 'ships', link: '/cruise-space?section=vip', matchType: 'content' }); }); } // 8. Guide Page Content const guidePage = t.guide; if (guidePage) { // Hero index.push({ title: guidePage.hero.title, subtitle: guidePage.hero.subtitle, category: 'activity', link: '/guide', matchType: 'title' }); // Travel Tips index.push({ title: guidePage.tips.sectionSubtitle, subtitle: guidePage.tips.sectionTitle, category: 'activity', link: '/guide?section=tips', matchType: 'title' }); // Tips Categories guidePage.tips.categories?.forEach((category: any) => { index.push({ title: category.title, category: 'activity', link: '/guide?section=tips', matchType: 'content' }); // Tips Items category.items?.forEach((item: string) => { index.push({ title: item, category: 'activity', link: '/guide?section=tips', matchType: 'content' }); }); }); // FAQ index.push({ title: guidePage.faq.sectionSubtitle, subtitle: guidePage.faq.sectionTitle, category: 'activity', link: '/guide?section=faq', matchType: 'title' }); // FAQ Items guidePage.faq.items?.forEach((faq: any) => { index.push({ title: faq.question, subtitle: faq.answer, category: 'activity', link: '/guide?section=faq', matchType: 'content' }); }); } return index; }, [language, t, itineraries, ships, dining, videoSection]); // Generate Search Suggestions const suggestions = useMemo(() => { if (!query.trim() || query.length < 2 || !showSuggestions) return []; const lowerQuery = query.toLowerCase(); // Extract unique terms from search index const allTerms = new Set(); searchIndex.forEach(item => { // Split title into words item.title.toLowerCase().split(/\s+/).forEach(word => { if (word.length > 2) allTerms.add(word); }); // Split subtitle into words if exists if (item.subtitle) { item.subtitle.toLowerCase().split(/\s+/).forEach(word => { if (word.length > 2) allTerms.add(word); }); } }); // Find terms that start with or contain the query const matchingTerms = Array.from(allTerms) .filter(term => term.startsWith(lowerQuery)) .sort(); // Limit to top 5 suggestions return matchingTerms.slice(0, 5).map(term => ({ text: term, count: searchIndex.filter(item => item.title.toLowerCase().includes(term) || (item.subtitle && item.subtitle.toLowerCase().includes(term)) ).length, category: 'all' })); }, [query, searchIndex, showSuggestions]); // Filter Results with Enhanced Matching and Grouping const filteredResults = useMemo(() => { if (!query.trim()) return []; const lowerQuery = query.toLowerCase(); // Calculate relevance score for each match const resultsWithScore = searchIndex .map(item => { const titleLower = item.title.toLowerCase(); const subtitleLower = item.subtitle?.toLowerCase() || ''; // Basic match check const titleMatches = titleLower.includes(lowerQuery); const subtitleMatches = subtitleLower.includes(lowerQuery); if (!titleMatches && !subtitleMatches) return null; // Calculate relevance score let score = 0; // Title matches are more important if (titleMatches) { score += 10; // Exact match in title gives extra points if (titleLower === lowerQuery) score += 20; // Start of title match gives more points if (titleLower.startsWith(lowerQuery)) score += 5; } // Subtitle matches are less important if (subtitleMatches) { score += 5; if (subtitleLower === lowerQuery) score += 10; if (subtitleLower.startsWith(lowerQuery)) score += 3; } // Match type bonus if (item.matchType === 'title') score += 3; // Category priority (home > itinerary > ships > dining > activity) const categoryPriority = { home: 5, itinerary: 4, ships: 3, dining: 2, activity: 1 }; score += categoryPriority[item.category] || 0; return { ...item, score }; }) .filter((item): item is typeof item & { score: number } => item !== null); // Sort by relevance score (descending) const sortedResults = resultsWithScore.sort((a, b) => b.score - a.score); return sortedResults; }, [query, searchIndex]); // Group results by category for better display const groupedResults = useMemo(() => { if (!filteredResults.length) return []; const categories = ['home', 'itinerary', 'ships', 'dining', 'activity'] as const; const groups: { category: string; results: typeof filteredResults }[] = []; categories.forEach(category => { const categoryResults = filteredResults.filter(item => item.category === category); if (categoryResults.length > 0) { groups.push({ category, results: categoryResults }); } }); return groups; }, [filteredResults]); const handleResultClick = (link: string) => { onClose(); if (link.startsWith('/?section=')) { const sectionId = link.split('=')[1]; navigate('/'); // Use a safer timeout and scroll logic setTimeout(() => { const element = document.getElementById(sectionId); if (element) { // Add some offset to account for fixed header const headerOffset = 80; const elementPosition = element.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - headerOffset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); } }, 150); } else if (link.includes('?section=')) { // Handle other section links with proper scroll logic const [path, queryString] = link.split('?'); const params = new URLSearchParams(queryString); const section = params.get('section'); navigate(path); if (section) { setTimeout(() => { const elementId = link.startsWith('/cruise-space') ? `space-${section}` : section; const element = document.getElementById(elementId); if (element) { const headerOffset = 80; const elementPosition = element.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - headerOffset; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); } }, 200); } } else { navigate(link); } }; if (!isOpen) return null; return ( // Outer Wrapper: Fully transparent, used for positioning and click-outside detection
{/* Inner Container: - Compact floating bar (capsule/rounded rect style) - Background: Semi-transparent dark glass (bg-black/60) to ensure readability - Dimensions: Tightly wrapped around text input (w-auto, min-width) */}
e.stopPropagation()} >
setQuery(e.target.value)} placeholder={searchT.placeholder} className="flex-1 bg-transparent border-none text-xl font-serif italic text-white placeholder-white/40 focus:outline-none focus:ring-0 transition-colors tracking-wide h-10 px-0" />
{/* Results Dropdown - Floating immediately below the bar */} {(query.trim() !== '') && (
{/* Search Suggestions */} {suggestions.length > 0 && showSuggestions && query.length < 20 && (
Suggestions
{suggestions.map((suggestion, idx) => (
{ setQuery(suggestion.text); setShowSuggestions(false); }} className="group flex items-center justify-between p-3 hover:bg-white/10 cursor-pointer transition-colors rounded-sm" >

{suggestion.text}

{suggestion.count} results

))}
)} {/* Search Results */} {(!suggestions.length || !showSuggestions) && (
{filteredResults.length > 0 ? (
{searchT.results_for} "{query}"
{groupedResults.map((group, groupIdx) => (
{group.category === 'itinerary' && } {group.category === 'ships' && } {group.category === 'dining' && } {group.category === 'activity' && } {group.category === 'home' && } {searchT.categories?.[group.category] || group.category} ({group.results.length})
{group.results.map((result, resultIdx) => (
handleResultClick(result.link)} className="group flex items-center justify-between p-3 hover:bg-white/10 cursor-pointer transition-colors rounded-sm mx-2" >

{result.title}

{result.subtitle && (

{result.subtitle}

)}
))}
))}
) : (
{searchT.no_results}
)}
)}
)}
); }; export default SearchOverlay;